From e1a133456bfeab0887eee1fe733fd8049d717fc9 Mon Sep 17 00:00:00 2001 From: Trevor Anderson <63077034+trande4884@users.noreply.github.com> Date: Tue, 9 May 2023 01:14:09 -0400 Subject: [PATCH 1/3] Migrating spring data cosmos from sdk/cosmos to sdk/spring (#33905) * Moving the azure-spring-data-cosmos and azure-spring-data-cosmos-test directories from sdk/cosmos to sdk/spring * Updating the azure-spring-data-cosmos-test pom file. * Updating the azure-spring-data-cosmos pom file. * Updating the changelog. * Making changes for PR. * Making changes for PR. * Update cspell to ignore these directories still. * Fixing paths to sdk/spring from sdk/cosmos. * Fixing more paths. * Fixing more paths. * Switching azure-spring-data-cosmos to use the dependency versoin of azure-cosmos and not the current version. * Fixing dependency. * Moving azure-spring-data-cosmos to the sdk/spring ci. * Updating CI pipelines to run correctly for the migration. 'java - cosmos - ci' had spring removed from it and put in it's own pipeline to accomdate the change, this new pipeline is 'java - cosmos - spring -ci' * Fixing tests. * Removing un-used dependency. * Resolving issue from merge conflict. * Fixing spring integration tests * Fixing spring integration tests. * Removing duplicate from POM. --- sdk/spring/CHANGELOG.md | 91 ++ .../CHANGELOG.md | 5 + .../azure-spring-data-cosmos-test/README.md | 9 + .../azure-spring-data-cosmos-test/pom.xml | 127 ++ ...tractIntegrationTestCollectionManager.java | 177 +++ .../spring/data/cosmos/ContainerLock.java | 212 +++ .../spring/data/cosmos/ContainerLockIT.java | 97 ++ .../data/cosmos/CosmosFactoryTestIT.java | 104 ++ .../IntegrationTestCollectionManager.java | 33 + ...ctiveIntegrationTestCollectionManager.java | 33 + .../data/cosmos/common/DynamicContainer.java | 16 + .../data/cosmos/common/PageTestUtils.java | 52 + .../common/ResponseDiagnosticsTestUtils.java | 37 + .../data/cosmos/common/TestConstants.java | 120 ++ .../spring/data/cosmos/common/TestUtils.java | 40 + .../config/AbstractCosmosConfigurationIT.java | 148 +++ .../data/cosmos/core/CosmosTemplateIT.java | 1045 +++++++++++++++ .../core/CosmosTemplatePartitionIT.java | 428 +++++++ .../MultiTenantContainerCosmosFactory.java | 34 + .../MultiTenantContainerCosmosFactoryIT.java | 135 ++ .../core/MultiTenantDBCosmosFactory.java | 32 + .../core/MultiTenantDBCosmosFactoryIT.java | 133 ++ .../cosmos/core/ReactiveCosmosTemplateIT.java | 742 +++++++++++ .../ReactiveCosmosTemplatePartitionIT.java | 303 +++++ ...veMultiTenantContainerCosmosFactoryIT.java | 139 ++ .../ReactiveMultiTenantDBCosmosFactoryIT.java | 137 ++ .../event/ApplicationContextEventErrorIT.java | 46 + ...pplicationContextEventErrorReactiveIT.java | 45 + .../event/ApplicationContextEventIT.java | 132 ++ .../ApplicationContextEventReactiveIT.java | 114 ++ .../event/ErrorEventListenerConfig.java | 14 + .../SimpleCosmosMappingEventListener.java | 16 + .../event/ThrowErrorEventListener.java | 12 + .../spring/data/cosmos/domain/Address.java | 96 ++ .../data/cosmos/domain/AuditableEntity.java | 79 ++ .../domain/AuditableIdGeneratedEntity.java | 70 + .../data/cosmos/domain/AutoScaleSample.java | 27 + .../domain/ComplexIndexPolicyEntity.java | 64 + .../cosmos/domain/CompositeIndexEntity.java | 72 ++ .../spring/data/cosmos/domain/Contact.java | 123 ++ .../spring/data/cosmos/domain/Course.java | 86 ++ .../data/cosmos/domain/CourseWithEtag.java | 100 ++ .../spring/data/cosmos/domain/Customer.java | 142 +++ .../data/cosmos/domain/GenIdEntity.java | 70 + .../spring/data/cosmos/domain/Importance.java | 7 + .../data/cosmos/domain/IndexPolicyEntity.java | 34 + .../domain/IndexPolicyOverwriteEntity.java | 57 + .../data/cosmos/domain/IntegerIdDomain.java | 70 + .../data/cosmos/domain/LongIdDomain.java | 70 + .../cosmos/domain/LongIdDomainPartition.java | 72 ++ .../azure/spring/data/cosmos/domain/Memo.java | 98 ++ .../data/cosmos/domain/NestedEntity.java | 49 + .../domain/NestedPartitionKeyEntity.java | 69 + ...dPartitionKeyEntityWithGeneratedValue.java | 69 + .../data/cosmos/domain/PageableMemo.java | 97 ++ .../data/cosmos/domain/PageablePerson.java | 129 ++ .../data/cosmos/domain/PartitionPerson.java | 115 ++ .../data/cosmos/domain/PersistableEntity.java | 77 ++ .../spring/data/cosmos/domain/Person.java | 156 +++ .../cosmos/domain/PersonCrossPartition.java | 157 +++ .../data/cosmos/domain/PersonWithEtag.java | 128 ++ .../spring/data/cosmos/domain/Project.java | 132 ++ .../spring/data/cosmos/domain/Question.java | 77 ++ .../data/cosmos/domain/ReactiveTeacher.java | 89 ++ .../azure/spring/data/cosmos/domain/Role.java | 110 ++ .../data/cosmos/domain/SortedProject.java | 132 ++ .../data/cosmos/domain/SpELBeanStudent.java | 82 ++ .../cosmos/domain/SpELPropertyStudent.java | 82 ++ .../spring/data/cosmos/domain/Student.java | 82 ++ .../spring/data/cosmos/domain/Teacher.java | 89 ++ .../data/cosmos/domain/TimeToLiveSample.java | 26 + .../data/cosmos/domain/UUIDIdDomain.java | 71 ++ .../cosmos/domain/UniqueKeyPolicyEntity.java | 75 ++ .../data/cosmos/domain/inheritance/Shape.java | 49 + .../cosmos/domain/inheritance/Square.java | 71 ++ .../repository/MultiCosmosTemplateIT.java | 92 ++ .../MultiTenantTestRepositoryConfig.java | 96 ++ .../ReactiveUUIDIdDomainRepositoryIT.java | 214 ++++ .../SecondaryTestRepositoryConfig.java | 105 ++ .../repository/StubAuditorProvider.java | 22 + .../repository/StubDateTimeProvider.java | 23 + .../repository/TestRepositoryConfig.java | 101 ++ .../repository/TestRepositorySpELConfig.java | 18 + .../integration/AddressRepositoryIT.java | 293 +++++ .../integration/AnnotatedQueryIT.java | 318 +++++ .../repository/integration/AuditableIT.java | 132 ++ .../integration/CompositeIndexIT.java | 161 +++ .../integration/ContactRepositoryIT.java | 254 ++++ .../integration/CosmosAnnotationIT.java | 139 ++ .../repository/integration/CountIT.java | 93 ++ .../integration/CustomerRepositoryIT.java | 89 ++ .../cosmos/repository/integration/EtagIT.java | 127 ++ .../integration/IndexPolicyUpdateIT.java | 159 +++ .../IntegerIdDomainRepositoryIT.java | 257 ++++ .../integration/LongIdDomainRepositoryIT.java | 284 +++++ .../integration/MemoRepositoryIT.java | 247 ++++ .../NestedPartitionKeyRepositoryIT.java | 116 ++ .../PageableAddressRepositoryIT.java | 180 +++ .../integration/PageableMemoRepositoryIT.java | 209 +++ .../PageablePersonRepositoryIT.java | 117 ++ .../repository/integration/PersistableIT.java | 117 ++ .../integration/PersonCrossPartitionIT.java | 287 +++++ .../integration/ProjectRepositoryIT.java | 563 ++++++++ .../integration/ProjectRepositorySortIT.java | 233 ++++ .../integration/QuestionRepositoryIT.java | 83 ++ .../integration/ReactiveAuditableIT.java | 135 ++ .../ReactiveCourseRepositoryIT.java | 381 ++++++ .../integration/ReactiveEtagIT.java | 127 ++ .../ReactiveIndexPolicyUpdateIT.java | 160 +++ ...dDomainPartitionPartitionRepositoryIT.java | 251 ++++ ...eactiveNestedPartitionKeyRepositoryIT.java | 122 ++ .../integration/ReactiveRoleRepositoryIT.java | 97 ++ .../ReactiveTeacherRepositoryIT.java | 139 ++ .../integration/SpELCosmosAnnotationIT.java | 118 ++ .../integration/SquareRepositoryIT.java | 62 + .../integration/StudentRepositoryIT.java | 229 ++++ .../integration/TeacherRepositoryIT.java | 70 + .../integration/UUIDIdDomainRepositoryIT.java | 235 ++++ .../integration/UniqueKeyPolicyIT.java | 162 +++ .../repository/AddressRepository.java | 65 + .../AuditableIdGeneratedRepository.java | 9 + .../repository/AuditableRepository.java | 17 + .../repository/ContactRepository.java | 53 + .../repository/CustomerRepository.java | 10 + .../repository/IntegerIdDomainRepository.java | 12 + .../repository/LongIdDomainRepository.java | 12 + .../repository/repository/MemoRepository.java | 37 + .../NestedPartitionKeyRepository.java | 12 + .../repository/PageableAddressRepository.java | 17 + .../repository/PageableMemoRepository.java | 15 + .../repository/PageablePersonRepository.java | 11 + .../PersistableEntityRepository.java | 12 + .../repository/PersonWithEtagRepository.java | 11 + .../repository/ProjectRepository.java | 95 ++ .../repository/QuestionRepository.java | 9 + .../repository/ReactiveAddressRepository.java | 23 + ...eactiveAuditableIdGeneratedRepository.java | 9 + .../ReactiveAuditableRepository.java | 9 + .../repository/ReactiveCourseRepository.java | 75 ++ .../ReactiveCourseWithEtagRepository.java | 10 + ...activeLongIdDomainPartitionRepository.java | 12 + .../ReactiveNestedPartitionKeyRepository.java | 12 + .../ReactivePersistableEntityRepository.java | 12 + .../repository/ReactiveRoleRepository.java | 26 + .../repository/ReactiveTeacherRepository.java | 28 + .../ReactiveUUIDIdDomainRepository.java | 14 + .../repository/SortedProjectRepository.java | 20 + .../repository/SquareRepository.java | 12 + .../repository/StudentRepository.java | 98 ++ .../repository/TeacherRepository.java | 17 + .../repository/UUIDIdDomainRepository.java | 14 + .../UniqueKeyPolicyEntityRepository.java | 12 + .../src/test/resources/application.properties | 27 + .../src/test/resources/application.yml | 3 + .../src/test/resources/logback-test.xml | 15 + .../azure-spring-data-cosmos/CHANGELOG.md | 348 +++++ .../azure-spring-data-cosmos/CONTRIBUTING.md | 69 + sdk/spring/azure-spring-data-cosmos/README.md | 1055 +++++++++++++++ sdk/spring/azure-spring-data-cosmos/pom.xml | 224 ++++ .../azure/spring/data/cosmos/Constants.java | 97 ++ .../spring/data/cosmos/CosmosFactory.java | 111 ++ .../data/cosmos/common/CosmosUtils.java | 93 ++ .../cosmos/common/ExpressionResolver.java | 44 + .../spring/data/cosmos/common/MacAddress.java | 156 +++ .../spring/data/cosmos/common/Memoizer.java | 35 + .../data/cosmos/common/PropertyLoader.java | 56 + .../data/cosmos/common/package-info.java | 7 + .../config/AbstractCosmosConfiguration.java | 107 ++ .../data/cosmos/config/CosmosConfig.java | 293 +++++ .../config/CosmosConfigurationSupport.java | 113 ++ .../config/DatabaseThroughputConfig.java | 50 + .../data/cosmos/config/package-info.java | 7 + .../data/cosmos/core/CosmosOperations.java | 390 ++++++ .../data/cosmos/core/CosmosTemplate.java | 1132 +++++++++++++++++ .../cosmos/core/ReactiveCosmosOperations.java | 324 +++++ .../cosmos/core/ReactiveCosmosTemplate.java | 933 ++++++++++++++ .../data/cosmos/core/ResponseDiagnostics.java | 115 ++ .../core/ResponseDiagnosticsProcessor.java | 26 + .../core/convert/MappingCosmosConverter.java | 239 ++++ .../core/convert/ObjectMapperFactory.java | 34 + .../cosmos/core/convert/package-info.java | 7 + .../generator/AbstractQueryGenerator.java | 321 +++++ .../core/generator/CountQueryGenerator.java | 17 + .../generator/FindQuerySpecGenerator.java | 22 + .../core/generator/NativeQueryGenerator.java | 76 ++ .../core/generator/QuerySpecGenerator.java | 19 + .../cosmos/core/generator/package-info.java | 7 + .../mapping/BasicCosmosPersistentEntity.java | 61 + .../BasicCosmosPersistentProperty.java | 45 + .../cosmos/core/mapping/CompositeIndex.java | 23 + .../core/mapping/CompositeIndexPath.java | 32 + .../data/cosmos/core/mapping/Container.java | 62 + .../core/mapping/CosmosAuditingRegistrar.java | 75 ++ .../core/mapping/CosmosIndexingPolicy.java | 62 + .../core/mapping/CosmosMappingContext.java | 49 + .../core/mapping/CosmosPersistentEntity.java | 24 + .../mapping/CosmosPersistentProperty.java | 12 + .../core/mapping/CosmosSimpleTypes.java | 43 + .../cosmos/core/mapping/CosmosUniqueKey.java | 41 + .../core/mapping/CosmosUniqueKeyPolicy.java | 30 + .../core/mapping/EnableCosmosAuditing.java | 56 + .../cosmos/core/mapping/GeneratedValue.java | 17 + .../cosmos/core/mapping/PartitionKey.java | 25 + .../event/AbstractCosmosEventListener.java | 57 + .../core/mapping/event/AfterLoadEvent.java | 46 + .../mapping/event/CosmosMappingEvent.java | 68 + .../core/mapping/event/package-info.java | 7 + .../cosmos/core/mapping/package-info.java | 7 + .../spring/data/cosmos/core/package-info.java | 8 + .../cosmos/core/query/CosmosPageImpl.java | 81 ++ .../cosmos/core/query/CosmosPageRequest.java | 143 +++ .../data/cosmos/core/query/CosmosQuery.java | 301 +++++ .../cosmos/core/query/CosmosSliceImpl.java | 59 + .../data/cosmos/core/query/Criteria.java | 109 ++ .../data/cosmos/core/query/CriteriaType.java | 328 +++++ .../data/cosmos/core/query/package-info.java | 7 + .../exception/ConfigurationException.java | 30 + .../exception/CosmosAccessException.java | 72 ++ .../exception/CosmosExceptionUtils.java | 67 + .../exception/DatabaseCreationException.java | 31 + .../exception/IllegalCollectionException.java | 31 + .../exception/IllegalQueryException.java | 31 + .../data/cosmos/exception/package-info.java | 7 + .../spring/data/cosmos/package-info.java | 7 + .../cosmos/repository/CosmosRepository.java | 77 ++ .../spring/data/cosmos/repository/Query.java | 24 + .../repository/ReactiveCosmosRepository.java | 69 + .../config/CosmosRepositoriesRegistrar.java | 27 + ...osmosRepositoryConfigurationExtension.java | 74 ++ .../config/EnableCosmosRepositories.java | 104 ++ .../EnableReactiveCosmosRepositories.java | 104 ++ .../ReactiveCosmosRepositoriesRegistrar.java | 27 + ...osmosRepositoryConfigurationExtension.java | 72 ++ .../repository/config/package-info.java | 7 + .../data/cosmos/repository/package-info.java | 7 + .../repository/query/AbstractCosmosQuery.java | 131 ++ .../query/AbstractReactiveCosmosQuery.java | 123 ++ .../query/CosmosEntityMetadata.java | 17 + .../repository/query/CosmosParameter.java | 26 + .../query/CosmosParameterAccessor.java | 18 + .../CosmosParameterParameterAccessor.java | 34 + .../repository/query/CosmosParameters.java | 38 + .../repository/query/CosmosQueryCreator.java | 96 ++ .../query/CosmosQueryExecution.java | 250 ++++ .../repository/query/CosmosQueryMethod.java | 77 ++ .../repository/query/PartTreeCosmosQuery.java | 68 + .../query/PartTreeReactiveCosmosQuery.java | 63 + .../query/ReactiveCosmosEntityMetadata.java | 17 + .../ReactiveCosmosParameterAccessor.java | 18 + ...ctiveCosmosParameterParameterAccessor.java | 35 + .../query/ReactiveCosmosQueryCreator.java | 94 ++ .../query/ReactiveCosmosQueryExecution.java | 169 +++ .../query/ReactiveCosmosQueryMethod.java | 90 ++ .../query/SimpleCosmosEntityMetadata.java | 56 + .../SimpleReactiveCosmosEntityMetadata.java | 53 + .../cosmos/repository/query/package-info.java | 7 + .../support/CosmosEntityInformation.java | 711 +++++++++++ .../support/CosmosRepositoryFactory.java | 86 ++ .../support/CosmosRepositoryFactoryBean.java | 73 ++ .../support/IndexPolicyCompareService.java | 65 + .../ReactiveCosmosRepositoryFactory.java | 88 ++ .../ReactiveCosmosRepositoryFactoryBean.java | 75 ++ .../support/SimpleCosmosRepository.java | 327 +++++ .../SimpleReactiveCosmosRepository.java | 266 ++++ .../support/StringBasedCosmosQuery.java | 139 ++ .../StringBasedReactiveCosmosQuery.java | 118 ++ .../repository/support/package-info.java | 7 + .../src/main/java/module-info.java | 33 + .../azure-spring-data-cosmos.properties | 1 + .../main/resources/META-INF/spring.factories | 1 + ...riesUserReactiveRepositoryCodeSnippet.java | 26 + ...tatedQueriesUserRepositoryCodeSnippet.java | 22 + .../spring/data/cosmos/AppConfiguration.java | 94 ++ .../cosmos/AppConfigurationCodeSnippet.java | 86 ++ .../spring/data/cosmos/AuditableUser.java | 27 + .../CosmosIndexingPolicyCodeSnippet.java | 23 + .../spring/data/cosmos/CosmosProperties.java | 94 ++ .../CosmosUniqueKeyPolicyCodeSnippet.java | 29 + .../spring/data/cosmos/GeneratedIdEntity.java | 16 + .../cosmos/MultiTenantDBCosmosFactory.java | 33 + .../com/azure/spring/data/cosmos/MyItem.java | 16 + .../data/cosmos/NestedEntitySample.java | 9 + .../NestedPartitionKeyEntitySample.java | 13 + .../ObjectMapperConfigurationCodeSnippet.java | 19 + .../cosmos/PageableRepositoryCodeSnippet.java | 32 + .../spring/data/cosmos/SampleApplication.java | 45 + .../cosmos/SliceQueriesUserRepository.java | 16 + .../cosmos/SliceRepositoryCodeSnippet.java | 30 + .../com/azure/spring/data/cosmos/User.java | 58 + .../spring/data/cosmos/UserRepository.java | 15 + .../cosmos/UserRepositoryConfiguration.java | 79 ++ .../azure/spring/data/cosmos/UserSample.java | 15 + .../common/ExpressionResolverUnitTest.java | 43 + .../data/cosmos/common/MemoizerUnitTest.java | 49 + .../cosmos/common/PropertyLoaderUnitTest.java | 17 + .../data/cosmos/common/TestConstants.java | 72 ++ .../cosmos/core/CosmosFactoryUnitTest.java | 43 + .../core/CosmosTemplateIllegalTest.java | 113 ++ .../cosmos/core/CosmosTemplateUnitTest.java | 24 + .../ZonedDateTimeDeserializerTest.java | 49 + .../MappingCosmosConverterUnitTest.java | 157 +++ .../generator/AbstractQueryGeneratorTest.java | 139 ++ .../generator/NativeQueryGeneratorTest.java | 48 + .../BasicCosmosPersistentEntityUnitTest.java | 28 + .../mapping/CosmosMappingContextUnitTest.java | 29 + .../event/CosmosEventListenerUnitTests.java | 94 ++ .../core/query/CosmosQueryUnitTest.java | 30 + .../cosmos/core/query/CriteriaUnitTest.java | 76 ++ .../spring/data/cosmos/domain/Address.java | 85 ++ .../spring/data/cosmos/domain/IPerson.java | 7 + .../spring/data/cosmos/domain/Importance.java | 7 + .../data/cosmos/domain/LongIdDomain.java | 70 + .../azure/spring/data/cosmos/domain/Memo.java | 98 ++ .../cosmos/domain/NoDBAnnotationPerson.java | 107 ++ .../spring/data/cosmos/domain/Person.java | 128 ++ .../data/cosmos/domain/PersonWithEtag.java | 128 ++ .../azure/spring/data/cosmos/domain/Role.java | 105 ++ .../spring/data/cosmos/domain/Student.java | 82 ++ .../data/cosmos/domain/TimeToLiveSample.java | 26 + .../data/cosmos/domain/UUIDIdDomain.java | 71 ++ .../repository/CosmosAnnotationUnitTest.java | 129 ++ .../SimpleCosmosRepositoryIllegalTest.java | 71 ++ .../SimpleCosmosRepositoryUnitTest.java | 103 ++ ...ositoryConfigurationExtensionUnitTest.java | 55 + ...ositoryConfigurationExtensionUnitTest.java | 57 + .../query/AbstractCosmosQueryUnitTest.java | 81 ++ .../AbstractReactiveCosmosQueryUnitTest.java | 69 + .../repository/PersonRepository.java | 11 + .../CosmosEntityInformationUnitTest.java | 512 ++++++++ .../CosmosRepositoryFactoryBeanUnitTest.java | 29 + .../CosmosRepositoryFactoryUnitTest.java | 27 + .../src/test/resources/application.properties | 28 + sdk/spring/ci.yml | 11 + sdk/spring/cosmos-integration-matrix.json | 13 + sdk/spring/pom.xml | 4 + .../cosmos-spring/test-resources.json | 146 +++ sdk/spring/tests.yml | 1 + 337 files changed, 33432 insertions(+) create mode 100644 sdk/spring/azure-spring-data-cosmos-test/CHANGELOG.md create mode 100644 sdk/spring/azure-spring-data-cosmos-test/README.md create mode 100644 sdk/spring/azure-spring-data-cosmos-test/pom.xml create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/AbstractIntegrationTestCollectionManager.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ContainerLock.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ContainerLockIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/CosmosFactoryTestIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/IntegrationTestCollectionManager.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ReactiveIntegrationTestCollectionManager.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/DynamicContainer.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/PageTestUtils.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/ResponseDiagnosticsTestUtils.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestUtils.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/config/AbstractCosmosConfigurationIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplatePartitionIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantContainerCosmosFactory.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantContainerCosmosFactoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantDBCosmosFactory.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantDBCosmosFactoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplateIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplatePartitionIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveMultiTenantContainerCosmosFactoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveMultiTenantDBCosmosFactoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventErrorIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventErrorReactiveIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventReactiveIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ErrorEventListenerConfig.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/SimpleCosmosMappingEventListener.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ThrowErrorEventListener.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Address.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AuditableEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AuditableIdGeneratedEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AutoScaleSample.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/ComplexIndexPolicyEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/CompositeIndexEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Contact.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Course.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/CourseWithEtag.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Customer.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/GenIdEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Importance.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IndexPolicyEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IndexPolicyOverwriteEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IntegerIdDomain.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomain.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomainPartition.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Memo.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedPartitionKeyEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedPartitionKeyEntityWithGeneratedValue.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PageableMemo.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PageablePerson.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PartitionPerson.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersistableEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Person.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersonCrossPartition.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersonWithEtag.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Project.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Question.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/ReactiveTeacher.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Role.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SortedProject.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SpELBeanStudent.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SpELPropertyStudent.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Student.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Teacher.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/TimeToLiveSample.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/UUIDIdDomain.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/UniqueKeyPolicyEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/inheritance/Shape.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/inheritance/Square.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/MultiCosmosTemplateIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/MultiTenantTestRepositoryConfig.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/ReactiveUUIDIdDomainRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/SecondaryTestRepositoryConfig.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/StubAuditorProvider.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/StubDateTimeProvider.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/TestRepositoryConfig.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/TestRepositorySpELConfig.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AddressRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AnnotatedQueryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AuditableIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CompositeIndexIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ContactRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CosmosAnnotationIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CountIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CustomerRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/EtagIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/IndexPolicyUpdateIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/IntegerIdDomainRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/LongIdDomainRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/MemoRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/NestedPartitionKeyRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageableAddressRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageableMemoRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageablePersonRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PersistableIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PersonCrossPartitionIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ProjectRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ProjectRepositorySortIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/QuestionRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveAuditableIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveEtagIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveIndexPolicyUpdateIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveLongIdDomainPartitionPartitionRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveNestedPartitionKeyRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveRoleRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveTeacherRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/SpELCosmosAnnotationIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/SquareRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/StudentRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/TeacherRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/UUIDIdDomainRepositoryIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/UniqueKeyPolicyIT.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AddressRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AuditableIdGeneratedRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AuditableRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ContactRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/CustomerRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/IntegerIdDomainRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/LongIdDomainRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/MemoRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/NestedPartitionKeyRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageableAddressRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageableMemoRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageablePersonRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersistableEntityRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersonWithEtagRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ProjectRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/QuestionRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAddressRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAuditableIdGeneratedRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAuditableRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseWithEtagRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveLongIdDomainPartitionRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveNestedPartitionKeyRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactivePersistableEntityRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveRoleRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveTeacherRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveUUIDIdDomainRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/SortedProjectRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/SquareRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/StudentRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/TeacherRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/UUIDIdDomainRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/UniqueKeyPolicyEntityRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/resources/application.properties create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/resources/application.yml create mode 100644 sdk/spring/azure-spring-data-cosmos-test/src/test/resources/logback-test.xml create mode 100644 sdk/spring/azure-spring-data-cosmos/CHANGELOG.md create mode 100644 sdk/spring/azure-spring-data-cosmos/CONTRIBUTING.md create mode 100644 sdk/spring/azure-spring-data-cosmos/README.md create mode 100644 sdk/spring/azure-spring-data-cosmos/pom.xml create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/Constants.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/CosmosFactory.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/CosmosUtils.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/ExpressionResolver.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/MacAddress.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/Memoizer.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/PropertyLoader.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/AbstractCosmosConfiguration.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/CosmosConfig.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/CosmosConfigurationSupport.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/DatabaseThroughputConfig.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ResponseDiagnostics.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ResponseDiagnosticsProcessor.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/MappingCosmosConverter.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/ObjectMapperFactory.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/AbstractQueryGenerator.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/CountQueryGenerator.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/FindQuerySpecGenerator.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/NativeQueryGenerator.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/QuerySpecGenerator.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentProperty.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CompositeIndex.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CompositeIndexPath.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/Container.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosAuditingRegistrar.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosIndexingPolicy.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosMappingContext.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosPersistentEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosPersistentProperty.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosSimpleTypes.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosUniqueKey.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosUniqueKeyPolicy.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/EnableCosmosAuditing.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/GeneratedValue.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/PartitionKey.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/AbstractCosmosEventListener.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/AfterLoadEvent.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/CosmosMappingEvent.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosPageImpl.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosPageRequest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosQuery.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosSliceImpl.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/Criteria.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CriteriaType.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/ConfigurationException.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/CosmosAccessException.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/CosmosExceptionUtils.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/DatabaseCreationException.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/IllegalCollectionException.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/IllegalQueryException.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/CosmosRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/Query.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/ReactiveCosmosRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoriesRegistrar.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoryConfigurationExtension.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/EnableCosmosRepositories.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/EnableReactiveCosmosRepositories.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoriesRegistrar.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoryConfigurationExtension.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQuery.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQuery.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosEntityMetadata.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameter.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameterAccessor.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameterParameterAccessor.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameters.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryCreator.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryExecution.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryMethod.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/PartTreeCosmosQuery.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/PartTreeReactiveCosmosQuery.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosEntityMetadata.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosParameterAccessor.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosParameterParameterAccessor.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryCreator.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryExecution.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryMethod.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/SimpleCosmosEntityMetadata.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/SimpleReactiveCosmosEntityMetadata.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosEntityInformation.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactory.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryBean.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/IndexPolicyCompareService.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactory.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactoryBean.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleCosmosRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleReactiveCosmosRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedCosmosQuery.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedReactiveCosmosQuery.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/package-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/java/module-info.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/resources/META-INF/azure-spring-data-cosmos.properties create mode 100644 sdk/spring/azure-spring-data-cosmos/src/main/resources/META-INF/spring.factories create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AnnotatedQueriesUserReactiveRepositoryCodeSnippet.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AnnotatedQueriesUserRepositoryCodeSnippet.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AppConfiguration.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AppConfigurationCodeSnippet.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AuditableUser.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosIndexingPolicyCodeSnippet.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosProperties.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosUniqueKeyPolicyCodeSnippet.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/GeneratedIdEntity.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/MultiTenantDBCosmosFactory.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/MyItem.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/NestedEntitySample.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/NestedPartitionKeyEntitySample.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/ObjectMapperConfigurationCodeSnippet.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/PageableRepositoryCodeSnippet.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SampleApplication.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SliceQueriesUserRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SliceRepositoryCodeSnippet.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/User.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserRepositoryConfiguration.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserSample.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/ExpressionResolverUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/MemoizerUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/PropertyLoaderUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosFactoryUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIllegalTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/convert/ZonedDateTimeDeserializerTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/converter/MappingCosmosConverterUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/generator/AbstractQueryGeneratorTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/generator/NativeQueryGeneratorTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentEntityUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/CosmosMappingContextUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/CosmosEventListenerUnitTests.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/query/CosmosQueryUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/query/CriteriaUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Address.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/IPerson.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Importance.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomain.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Memo.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/NoDBAnnotationPerson.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Person.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/PersonWithEtag.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Role.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Student.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/TimeToLiveSample.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/UUIDIdDomain.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/CosmosAnnotationUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/SimpleCosmosRepositoryIllegalTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/SimpleCosmosRepositoryUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoryConfigurationExtensionUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoryConfigurationExtensionUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQueryUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQueryUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersonRepository.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosEntityInformationUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryBeanUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryUnitTest.java create mode 100644 sdk/spring/azure-spring-data-cosmos/src/test/resources/application.properties create mode 100644 sdk/spring/cosmos-integration-matrix.json create mode 100644 sdk/spring/spring-cloud-azure-integration-tests/test-resources/cosmos-spring/test-resources.json diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index 47499c43c99f..8e519dd29b86 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -59,6 +59,97 @@ This section includes changes in `spring-cloud-azure-appconfiguration-config` an #### Bugs Fixed - Fixes issue where credential from Azure Spring global properties was being overridden [#34695](https://github.com/Azure/azure-sdk-for-java/pull/34695). +## 4.7.0 (2023-03-23) +- This release is compatible with Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.14, 2.7.0-2.7.9. (Note: 2.5.x (x>14), 2.6.y (y>14) and 2.7.z (z>9) should be supported, but they aren't tested with this release.) +- This release is compatible with Spring Cloud 2020.0.3-2020.0.6, 2021.0.0-2021.0.5. (Note: 2020.0.x (x>6) and 2021.0.y (y>5) should be supported, but they aren't tested with this release.) + +### Spring Cloud Azure Dependencies (BOM) + +#### Dependency Updates +- Upgrade `azure-sdk-bom` to 1.2.11. +- Upgrade `azure-spring-data-cosmos` to 3.33.0. +- Upgrade `azure-resourcemanager` to 2.24.0. + +#### Features Added +- Added the following artifacts into current bom file: + - spring-cloud-azure-starter-appconfiguration-config + - spring-cloud-azure-appconfiguration-config + - spring-cloud-azure-appconfiguration-config-web + - spring-cloud-azure-feature-management + - spring-cloud-azure-feature-management-web + +#### Features Added +- The module `azure-spring-data-cosmos` was moved from sdk/cosmos to sdk/spring - See [PR 33905](https://github.com/Azure/azure-sdk-for-java/pull/33905) + +### Spring Messaging Event Hubs +This section includes changes in `spring-messaging-azure-eventhubs` module. + +#### Bugs Fixed +- Fixed Event Hubs Message header `source-type` from `kafka` to `amqp` when using `StreamBridge#send`. [#32777](https://github.com/Azure/azure-sdk-for-java/issues/32777) + +### Spring Cloud Stream Event Hubs Binder +This section includes changes in `spring-cloud-azure-stream-binder-eventhubs` module. + +#### Breaking Changes +- Make the default partition behavior of Spring Cloud Azure EventHubs binder be a round-robin assignment to align with Event Hubs.[#32816](https://github.com/Azure/azure-sdk-for-java/pull/32816). + +### Spring Cloud Azure Autoconfigure +This section includes changes in `spring-cloud-azure-autoconfigure` module. + +#### Features Added +- Support passwordless connections for JMS ServiceBus in Spring. [#33489](https://github.com/Azure/azure-sdk-for-java/pull/33489) + +#### Breaking Changes +- Deprecated properties for AAD and AAD B2C. [#33538](https://github.com/Azure/azure-sdk-for-java/pull/33538). + - Deprecated properties `spring.cloud.azure.active-directory.jwt-connect-timeout`, `spring.cloud.azure.active-directory.jwt-read-timeout`, `spring.cloud.azure.active-directory.jwt-size-limit`, if you want to configure them, please provide a RestOperations bean. + - Deprecated properties `spring.cloud.azure.active-directory.b2c.jwt-connect-timeout`, `spring.cloud.azure.active-directory.b2c.jwt-read-timeout`, `spring.cloud.azure.active-directory.b2c.jwt-size-limit`, if you want to configure them, please provide a RestOperations bean. + +## 4.6.0 (2023-02-07) +- This release is compatible with Spring Boot 2.5.0-2.5.14, 2.6.0-2.6.14, 2.7.0-2.7.8. (Note: 2.5.x (x>14), 2.6.y (y>14) and 2.7.z (z>8) should be supported, but they aren't tested with this release.) +- This release is compatible with Spring Cloud 2020.0.3-2020.0.6, 2021.0.0-2021.0.5. (Note: 2020.0.x (x>6) and 2021.0.y (y>5) should be supported, but they aren't tested with this release.) + +#### Features Added +- Release the `spring-cloud-azure-starter-redis`. This starter supports Azure hosted Redis service authenticating with Azure AD. + +### Spring Cloud Azure Autoconfigure +This section includes changes in `spring-cloud-azure-autoconfigure` module. + +#### Features Added +- Enhance the Event Hubs/Service Bus/Storage Queue message converter to support Spring autoconfiguration-pattern. [#30741](https://github.com/Azure/azure-sdk-for-java/issues/30741) +- Support the PROPERTY_AZURE_CLIENT_CERTIFICATE_PASSWORD property. [#30252](https://github.com/Azure/azure-sdk-for-java/issues/30252) +- Make the domain-name option optional when configuring Event Hubs/Service Bus in non-public Azure cloud [#32034](https://github.com/Azure/azure-sdk-for-java/issues/32034). + +#### Breaking Changes +- Delete properties: `spring.jms.serviebus.username`, `spring.jms.serviebus.password` and `spring.jms.serviebus.remote-uri` [#32467](https://github.com/Azure/azure-sdk-for-java/pull/32467). +- Change the default value of `spring.jms.servicebus.idle-timeout` from 30 minutes to 2 minutes [#32799](https://github.com/Azure/azure-sdk-for-java/pull/32799). +- Change the default value of `spring.cloud.azure.eventhubs.processor.load-balancing.strategy` from `BALANCED` to `GREEDY` [#32897](https://github.com/Azure/azure-sdk-for-java/pull/32897). + +#### Bugs Fixed +- Fixed `GraphClient` exceptions handling when switching `HttpURLConnection` to `RestTemplate`. [#32779](https://github.com/Azure/azure-sdk-for-java/issues/32779) + +#### Dependency Updates +- Upgrade Azure SDK BOM to 1.2.9. +- Upgrade Azure Spring Data Cosmos to 3.31.0. +- Upgrade Azure Resource Manager to 2.23.0. + +### Spring Cloud Azure Core +This section includes changes in `spring-cloud-azure-core`, `spring-cloud-azure-service`, and `spring-cloud-azure-resourcemanager` modules. + +#### Bugs Fixed +- Remove warning logs about client properties while using Kafka passwordless. [#32235](https://github.com/Azure/azure-sdk-for-java/issues/32235) + +### Spring Messaging Event Hubs +This section includes changes in `spring-messaging-azure-eventhubs` module. + +#### Breaking Changes +- Change the default load-balancing strategy from `BALANCED` to `GREEDY` [#32897](https://github.com/Azure/azure-sdk-for-java/pull/32897). + +### Spring Cloud Stream Event Hubs Binder +This section includes changes in `spring-cloud-azure-stream-binder-eventhubs` module. + +#### Breaking Changes +- Change the default value of `spring.cloud.stream.eventhubs.bindings..consumer.load-balancing.strategy` from `BALANCED` to `GREEDY` [#32897](https://github.com/Azure/azure-sdk-for-java/pull/32897). + ## 5.0.0 (2023-01-17) - This release is compatible with Spring Boot 3.0.0-3.0.1. (Note: 3.0.x (x>1) should be supported, but they aren't tested with this release.) - This release is compatible with Spring Cloud 2022.0.0. (Note: 2022.0.x (x>0) should be supported, but they aren't tested with this release.) diff --git a/sdk/spring/azure-spring-data-cosmos-test/CHANGELOG.md b/sdk/spring/azure-spring-data-cosmos-test/CHANGELOG.md new file mode 100644 index 000000000000..18dece4aff0d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/CHANGELOG.md @@ -0,0 +1,5 @@ +# Release History + +## 3.0.0-beta.1 (Unreleased) + + diff --git a/sdk/spring/azure-spring-data-cosmos-test/README.md b/sdk/spring/azure-spring-data-cosmos-test/README.md new file mode 100644 index 000000000000..ed36c42034ed --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/README.md @@ -0,0 +1,9 @@ +# Azure Spring Data Test client library for Java + +## Key concepts +## Getting started +## Key concepts +## Examples +## Troubleshooting +## Next steps +## Contributing diff --git a/sdk/spring/azure-spring-data-cosmos-test/pom.xml b/sdk/spring/azure-spring-data-cosmos-test/pom.xml new file mode 100644 index 000000000000..fd689499a84a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.9 + + + + com.azure + azure-spring-data-cosmos-test + 3.0.0-beta.1 + jar + Spring Data Test for Azure Cosmos DB SQL API + Spring Data Test for Azure Cosmos DB SQL API + https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-data-cosmos-test + + + azure-spring-data-cosmos-test + testdb-${maven.build.timestamp} + true + false + false + + + + + com.azure + azure-spring-data-cosmos + 3.35.0-beta.1 + + + + org.junit.vintage + junit-vintage-engine + test + + + org.hamcrest + hamcrest-core + + + + + org.mockito + mockito-core + test + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + org.slf4j + slf4j-simple + test + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M7 + + src/test/resources/application.properties + ${skip.integration.tests} + + + + integration-test + + integration-test + verify + + + + + + + + + + integration-test-azure + + integration-test-azure + false + true + false + + + + integration-test-emulator + + integration-test-emulator + false + false + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.0.0-M7 + + + + **/PageablePersonRepositoryIT.java + + + + + + + + diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/AbstractIntegrationTestCollectionManager.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/AbstractIntegrationTestCollectionManager.java new file mode 100644 index 000000000000..ca91a268026e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/AbstractIntegrationTestCollectionManager.java @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public abstract class AbstractIntegrationTestCollectionManager implements TestRule { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractIntegrationTestCollectionManager.class); + private static final Duration LEASE_DURATION = Duration.ofMinutes(5); + private static final ConcurrentMap CONTAINER_CLEANUP_REGISTRY = new ConcurrentHashMap<>(); + + static { + // since collections are sometimes re-used between tests, wait until the end of the test run to delete them + Runtime.getRuntime().addShutdownHook(new Thread(AbstractIntegrationTestCollectionManager::deleteRegisteredCollections)); + } + + public static void registerContainerForCleanup(Object template, String containerName) { + DeleteContainerAction action; + if (template instanceof CosmosTemplate) { + action = new DeleteContainerAction((CosmosTemplate) template, containerName); + } else if (template instanceof ReactiveCosmosTemplate) { + action = new DeleteContainerAction((ReactiveCosmosTemplate) template, containerName); + } else { + throw new IllegalStateException("Template must be instance of CosmosTemplate or ReactiveCosmosTemplate, was " + template); + } + CONTAINER_CLEANUP_REGISTRY.putIfAbsent(containerName, action); + } + + private static void deleteRegisteredCollections() { + CONTAINER_CLEANUP_REGISTRY.values().forEach(DeleteContainerAction::deleteContainer); + } + + protected T template; + private Map containerRefs = new HashMap<>(); + private boolean isSetupDone; + + protected abstract ContainerLock createLock(CosmosEntityInformation entityInfo, Duration leaseDuration); + protected abstract CosmosContainerProperties createContainerIfNotExists(CosmosEntityInformation entityInfo); + protected abstract void deleteContainerData(CosmosEntityInformation entityInfo); + protected abstract void deleteContainer(CosmosEntityInformation entityInfo); + + public void ensureContainersCreated(T template, Class... entityTypes) { + if (!isSetupDone) { + this.template = template; + initContainerRefs(entityTypes); + isSetupDone = true; + } else { + refreshContainerLeases(); + } + } + + private void initContainerRefs(Class[] entityTypes) { + for (Class entityType : entityTypes) { + CosmosEntityInformation entityInfo = new CosmosEntityInformation(entityType); + CosmosContainerProperties properties = createContainerIfNotExists(entityInfo); + registerContainerForCleanup(template, entityInfo.getContainerName()); + ContainerLock lock = createLock(entityInfo, LEASE_DURATION); + lock.acquire(LEASE_DURATION.multipliedBy(2)); + containerRefs.put(entityType, new ContainerRefs(entityInfo, properties, lock)); + } + } + + public void ensureContainersCreatedAndEmpty(T template, Class... entityTypes) { + ensureContainersCreated(template, entityTypes); + deleteContainerData(); + } + + public CosmosEntityInformation getEntityInformation(Class entityType) { + return containerRefs.get(entityType).cosmosEntityInformation; + } + + public CosmosContainerProperties getContainerProperties(Class entityType) { + return containerRefs.get(entityType).cosmosContainerProperties; + } + + public String getContainerName(Class entityType) { + return containerRefs.get(entityType).getContainerName(); + } + + private void deleteContainerData() { + for (ContainerRefs containerRef : containerRefs.values()) { + deleteContainerData(containerRef.cosmosEntityInformation); + } + } + + private void refreshContainerLeases() { + for (ContainerRefs containerRef : containerRefs.values()) { + containerRef.lock.renew(); + } + } + + private void releaseLocks() { + for (ContainerRefs containerRef : containerRefs.values()) { + try { + containerRef.lock.release(); + } catch (Exception ex) { + LOGGER.info("Failed to delete lock for container=" + containerRef.getContainerName()); + } + } + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + public void evaluate() throws Throwable { + try { + base.evaluate(); + } finally { + releaseLocks(); + } + } + }; + } + + private static class ContainerRefs { + + CosmosEntityInformation cosmosEntityInformation; + CosmosContainerProperties cosmosContainerProperties; + ContainerLock lock; + + public ContainerRefs(CosmosEntityInformation cosmosEntityInformation, CosmosContainerProperties cosmosContainerProperties, ContainerLock lock) { + this.cosmosEntityInformation = cosmosEntityInformation; + this.cosmosContainerProperties = cosmosContainerProperties; + this.lock = lock; + } + + public String getContainerName() { + return cosmosEntityInformation.getContainerName(); + } + + } + + private static class DeleteContainerAction { + + private CosmosTemplate template; + private ReactiveCosmosTemplate reactiveTemplate; + private String containerName; + + public DeleteContainerAction(CosmosTemplate template, String containerName) { + this.template = template; + this.containerName = containerName; + } + + public DeleteContainerAction(ReactiveCosmosTemplate reactiveTemplate, String containerName) { + this.reactiveTemplate = reactiveTemplate; + this.containerName = containerName; + } + + public void deleteContainer() { + try { + if (template != null) { + template.deleteContainer(containerName); + } else { + reactiveTemplate.deleteContainer(containerName); + } + } catch (Exception ex) { + LOGGER.info("Failed to delete container=" + containerName); + } + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ContainerLock.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ContainerLock.java new file mode 100644 index 000000000000..dc94aedeab2b --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ContainerLock.java @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; + +import java.time.Duration; +import java.time.OffsetDateTime; + +public class ContainerLock { + + private static CosmosEntityInformation lockEntityInfo; + + private LockStore lockStore; + private Duration leaseDuration; + private String lockName; + private LockEntry acquiredLock; + + public ContainerLock(CosmosTemplate template, String lockName, Duration leaseDuration) { + this.lockStore = new NonReactiveLockStore(template); + this.lockName = lockName; + this.leaseDuration = leaseDuration; + initLockContainer(lockStore, template); + } + + public ContainerLock(ReactiveCosmosTemplate reactiveTemplate, String lockName, Duration leaseDuration) { + this.lockStore = new ReactiveLockStore(reactiveTemplate); + this.lockName = lockName; + this.leaseDuration = leaseDuration; + initLockContainer(lockStore, reactiveTemplate); + } + + private static synchronized void initLockContainer(LockStore lockStore, Object template) { + if (lockEntityInfo == null) { + CosmosEntityInformation info = new CosmosEntityInformation<>(LockEntry.class); + lockStore.createContainerIfNotExists(info); + AbstractIntegrationTestCollectionManager.registerContainerForCleanup(template, info.getContainerName()); + lockEntityInfo = info; + } + } + + public void acquire(Duration tryForDuration) { + long started = System.currentTimeMillis(); + LockEntry entry = new LockEntry(lockName, OffsetDateTime.now().plus(leaseDuration)); + while (acquiredLock == null) { + try { + acquiredLock = lockStore.insertLock(entry); + } catch (Exception ex) { + if (shouldKeepTryingToAcquire(started, tryForDuration)) { + sleep(500); + releaseIfLeaseExpired(); + } else { + throw new LockAcquisitionFailedException(tryForDuration); + } + } + } + } + + private boolean shouldKeepTryingToAcquire(long started, Duration tryForDuration) { + long elapsedDuration = System.currentTimeMillis() - started; + return elapsedDuration <= tryForDuration.toMillis(); + } + + private void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + // ignored + } + } + + private void releaseIfLeaseExpired() { + LockEntry entry = lockStore.findActiveLock(lockName); + if (entry != null && entry.isLeaseExpired()) { + acquiredLock = entry; + release(); + } + } + + public void release() { + if (acquiredLock != null) { + lockStore.deleteLock(acquiredLock); + acquiredLock = null; + } + } + + public void renew() { + if (acquiredLock != null) { + acquiredLock.leaseExpiration = OffsetDateTime.now().plus(leaseDuration); + acquiredLock = lockStore.refreshLock(acquiredLock); + } + } + + public OffsetDateTime getLeaseExpiration() { + if (acquiredLock == null) { + return null; + } + return acquiredLock.leaseExpiration; + } + + private interface LockStore { + LockEntry insertLock(LockEntry entry); + LockEntry findActiveLock(String id); + LockEntry refreshLock(LockEntry entry); + void deleteLock(LockEntry entry); + void createContainerIfNotExists(CosmosEntityInformation entityInfo); + } + + private static class NonReactiveLockStore implements LockStore { + + private final CosmosTemplate template; + + public NonReactiveLockStore(CosmosTemplate template) { + this.template = template; + } + + @Override + public LockEntry insertLock(LockEntry entry) { + return template.insert(lockEntityInfo.getContainerName(), entry); + } + + @Override + public LockEntry findActiveLock(String id) { + return template.findById(lockEntityInfo.getContainerName(), id, LockEntry.class); + } + + @Override + public LockEntry refreshLock(LockEntry entry) { + return template.upsertAndReturnEntity(lockEntityInfo.getContainerName(), entry); + } + + @Override + public void deleteLock(LockEntry entry) { + template.deleteEntity(lockEntityInfo.getContainerName(), entry); + } + + @Override + public void createContainerIfNotExists(CosmosEntityInformation entityInfo) { + template.createContainerIfNotExists(entityInfo); + } + } + + private static class ReactiveLockStore implements LockStore { + + private final ReactiveCosmosTemplate template; + + public ReactiveLockStore(ReactiveCosmosTemplate template) { + this.template = template; + } + + @Override + public LockEntry insertLock(LockEntry entry) { + return template.insert(lockEntityInfo.getContainerName(), entry).block(); + } + + @Override + public LockEntry findActiveLock(String id) { + return template.findById(lockEntityInfo.getContainerName(), id, LockEntry.class).block(); + } + + @Override + public LockEntry refreshLock(LockEntry entry) { + return template.upsert(lockEntityInfo.getContainerName(), entry).block(); + } + + @Override + public void deleteLock(LockEntry entry) { + template.deleteEntity(lockEntityInfo.getContainerName(), entry).block(); + } + + @Override + public void createContainerIfNotExists(CosmosEntityInformation entityInfo) { + template.createContainerIfNotExists(entityInfo).block(); + } + } + + static class LockEntry { + @Id + @PartitionKey + public String id; + @Version + public String version; + public OffsetDateTime leaseExpiration; + + public LockEntry() { + } + + public LockEntry(String id, OffsetDateTime leaseExpiration) { + this.id = id; + this.leaseExpiration = leaseExpiration; + } + + @JsonIgnore + public boolean isLeaseExpired() { + return OffsetDateTime.now().isAfter(leaseExpiration); + } + + } + + static class LockAcquisitionFailedException extends RuntimeException { + public LockAcquisitionFailedException(Duration tryForDuration) { + super("Failed to acquire lock within " + tryForDuration); + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ContainerLockIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ContainerLockIT.java new file mode 100644 index 000000000000..bbe66f8dc73e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ContainerLockIT.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.After; +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.time.Duration; +import java.time.OffsetDateTime; + +import static org.junit.Assert.assertTrue; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ContainerLockIT { + + private static final Duration SHORT_LEASE_DURATION = Duration.ofSeconds(3); + + @Autowired + private CosmosTemplate template; + @Autowired + private CosmosTemplate reactiveTemplate; + private static CosmosTemplate staticTemplate; + + private ContainerLock lock; + private ContainerLock otherLock; + + @Before + public void setup() { + staticTemplate = template; + CosmosEntityInformation entityInfo = new CosmosEntityInformation(Address.class); + template.createContainerIfNotExists(entityInfo); + AbstractIntegrationTestCollectionManager.registerContainerForCleanup(template, entityInfo.getContainerName()); + lock = new ContainerLock(template, entityInfo.getContainerName(), SHORT_LEASE_DURATION); + otherLock = new ContainerLock(reactiveTemplate, entityInfo.getContainerName(), SHORT_LEASE_DURATION); + } + + @After + public void cleanup() { + releaseLockIgnoreException(lock); + releaseLockIgnoreException(otherLock); + } + + private void releaseLockIgnoreException(ContainerLock lock) { + try { + lock.release(); + } catch (Exception ex) { + // ignore + } + } + + @Test + public void shouldAcquireAndReleaseLock() { + lock.acquire(Duration.ofSeconds(1)); + lock.release(); + } + + @Test + public void acquireShouldAcquireLockIfLeaseExpires() { + OffsetDateTime start = OffsetDateTime.now(); + lock.acquire(Duration.ofSeconds(1)); + + otherLock.acquire(SHORT_LEASE_DURATION.plusSeconds(1)); + assertTrue(OffsetDateTime.now().isAfter(start.plus(SHORT_LEASE_DURATION))); + } + + @Test + public void acquireShouldThrowExceptionIfWaitForIsLessThanLeaseDuration() { + lock.acquire(Duration.ofSeconds(1)); + + try { + otherLock.acquire(SHORT_LEASE_DURATION.minusSeconds(1)); + Assert.fail(); + } catch (ContainerLock.LockAcquisitionFailedException ex) { + } + } + + @Test + public void renewShouldRenewTheLeaseDuration() { + lock.acquire(Duration.ofSeconds(1)); + OffsetDateTime originalExpiration = lock.getLeaseExpiration(); + + lock.renew(); + assertTrue(lock.getLeaseExpiration().isAfter(originalExpiration)); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/CosmosFactoryTestIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/CosmosFactoryTestIT.java new file mode 100644 index 000000000000..1d13b90a1776 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/CosmosFactoryTestIT.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import org.apache.commons.lang3.reflect.FieldUtils; +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 java.lang.reflect.Field; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CosmosFactoryTestIT { + + @Value("${cosmos.uri:}") + private String cosmosDbUri; + + @Value("${cosmos.key:}") + private String cosmosDbKey; + + @Test + public void testNullKey() { + CosmosAsyncClient ignored = null; + try { + ignored = CosmosFactory.createCosmosAsyncClient(new CosmosClientBuilder() + .endpoint(cosmosDbUri) + .key(null)); + } catch (Exception e) { + assertThat(e instanceof NullPointerException).isTrue(); + } finally { + if (ignored != null) { + ignored.close(); + } + } + + ignored = null; + try { + ignored = CosmosFactory.createCosmosAsyncClient(new CosmosClientBuilder() + .endpoint(cosmosDbUri) + .key("")); + } catch (Exception e) { + assertThat(e instanceof IllegalArgumentException).isTrue(); + } finally { + if (ignored != null) { + ignored.close(); + } + } + } + + @Test + public void testNullEndpoint() { + CosmosAsyncClient ignored = null; + try { + ignored = CosmosFactory.createCosmosAsyncClient(new CosmosClientBuilder() + .endpoint(null) + .key(cosmosDbKey)); + } catch (Exception e) { + assertThat(e instanceof NullPointerException).isTrue(); + } finally { + if (ignored != null) { + ignored.close(); + } + } + + ignored = null; + try { + ignored = CosmosFactory.createCosmosAsyncClient(new CosmosClientBuilder() + .endpoint("") + .key(cosmosDbKey)); + } catch (Exception e) { + assertThat(e instanceof IllegalArgumentException).isTrue(); + } finally { + if (ignored != null) { + ignored.close(); + } + } + } + + @Test + public void testConnectionPolicyUserAgentKept() throws IllegalAccessException { + + final CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder() + .endpoint(cosmosDbUri) + .key(cosmosDbKey); + CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + + final String uaSuffix = getUserAgentSuffixValue(cosmosClientBuilder); + assertThat(uaSuffix).contains(Constants.USER_AGENT_SUFFIX); + } + + private String getUserAgentSuffixValue(CosmosClientBuilder cosmosClientBuilder) throws IllegalAccessException { + final Field userAgentSuffix = FieldUtils.getDeclaredField(CosmosClientBuilder.class, + "userAgentSuffix", true); + return (String) userAgentSuffix.get(cosmosClientBuilder); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/IntegrationTestCollectionManager.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/IntegrationTestCollectionManager.java new file mode 100644 index 000000000000..33d6a5da89ae --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/IntegrationTestCollectionManager.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; + +import java.time.Duration; + +public class IntegrationTestCollectionManager extends AbstractIntegrationTestCollectionManager { + + @Override + public ContainerLock createLock(CosmosEntityInformation entityInfo, Duration leaseDuration) { + return new ContainerLock(template, entityInfo.getContainerName(), leaseDuration); + } + + @Override + public CosmosContainerProperties createContainerIfNotExists(CosmosEntityInformation entityInfo) { + return template.createContainerIfNotExists(entityInfo); + } + + @Override + public void deleteContainerData(CosmosEntityInformation entityInfo) { + template.deleteAll(entityInfo.getContainerName(), entityInfo.getJavaType()); + } + + @Override + public void deleteContainer(CosmosEntityInformation entityInfo) { + template.deleteContainer(entityInfo.getContainerName()); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ReactiveIntegrationTestCollectionManager.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ReactiveIntegrationTestCollectionManager.java new file mode 100644 index 000000000000..7c30c0fee44e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/ReactiveIntegrationTestCollectionManager.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; + +import java.time.Duration; + +public class ReactiveIntegrationTestCollectionManager extends AbstractIntegrationTestCollectionManager { + + @Override + public ContainerLock createLock(CosmosEntityInformation entityInfo, Duration leaseDuration) { + return new ContainerLock(template, entityInfo.getContainerName(), leaseDuration); + } + + @Override + public CosmosContainerProperties createContainerIfNotExists(CosmosEntityInformation entityInfo) { + return template.createContainerIfNotExists(entityInfo).block().getProperties(); + } + + @Override + public void deleteContainerData(CosmosEntityInformation entityInfo) { + template.deleteAll(entityInfo.getContainerName(), entityInfo.getJavaType()).block(); + } + + @Override + public void deleteContainer(CosmosEntityInformation entityInfo) { + template.deleteContainer(entityInfo.getContainerName()); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/DynamicContainer.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/DynamicContainer.java new file mode 100644 index 000000000000..f7e486aa23b9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/DynamicContainer.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.common; + +public class DynamicContainer { + private final String containerName; + + public DynamicContainer(String containerName) { + this.containerName = containerName; + } + + public String getContainerName() { + return this.containerName; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/PageTestUtils.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/PageTestUtils.java new file mode 100644 index 000000000000..36ce7dc5bff2 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/PageTestUtils.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.common; + +import com.azure.spring.data.cosmos.core.convert.ObjectMapperFactory; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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 { + + private static final ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper(); + + 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); + assertThat(page.hasNext()).isFalse(); + } + + 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); + assertThat(page.hasNext()).isTrue(); + } + + private static boolean continuationTokenIsNull(CosmosPageRequest pageRequest) { + final String tokenJson = pageRequest.getRequestContinuation(); + if (tokenJson == null) { + return true; + } + try { + JsonNode jsonNode = objectMapper.readTree(tokenJson); + return jsonNode.get("compositeToken") == null; + } catch (JsonProcessingException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/ResponseDiagnosticsTestUtils.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/ResponseDiagnosticsTestUtils.java new file mode 100644 index 000000000000..03d12c2048ee --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/ResponseDiagnosticsTestUtils.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.common; + +import com.azure.cosmos.CosmosDiagnostics; +import com.azure.spring.data.cosmos.core.ResponseDiagnostics; +import com.azure.spring.data.cosmos.core.ResponseDiagnosticsProcessor; + + +public class ResponseDiagnosticsTestUtils { + + private final ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + private ResponseDiagnostics diagnostics; + + public ResponseDiagnosticsTestUtils() { + responseDiagnosticsProcessor = responseDiagnostics -> { + diagnostics = responseDiagnostics; + }; + } + + public CosmosDiagnostics getCosmosDiagnostics() { + return diagnostics == null ? null : diagnostics.getCosmosDiagnostics(); + } + + public ResponseDiagnostics.CosmosResponseStatistics getCosmosResponseStatistics() { + return diagnostics == null ? null : diagnostics.getCosmosResponseStatistics(); + } + + public ResponseDiagnosticsProcessor getResponseDiagnosticsProcessor() { + return responseDiagnosticsProcessor; + } + + public ResponseDiagnostics getDiagnostics() { + return diagnostics; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java new file mode 100644 index 000000000000..945e313ed70b --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.common; + +import com.azure.cosmos.models.IndexingMode; +import com.azure.spring.data.cosmos.domain.Address; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class TestConstants { + + public static final String AUTOSCALE_MAX_THROUGHPUT = "4000"; + + public static final String MULTI_PARTITION_THROUGHPUT = "12000"; + 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 String HOBBY1 = "photography"; + public static final String PATCH_HOBBY1 = "shopping"; + public static final List HOBBIES = Arrays.asList(HOBBY1, "fishing"); + public static final List PATCH_HOBBIES = Arrays.asList(HOBBY1, "fishing", PATCH_HOBBY1); + public static final List
ADDRESSES = Arrays.asList(ADDRESS_1, ADDRESS_2); + + public static final String ROLE_COLLECTION_NAME = "RoleCollectionName"; + public static final String BOOK_COLLECTION_NAME = "BookCollectionName"; + public static final int TIME_TO_LIVE = 5; + public static final boolean INDEXING_POLICY_AUTOMATIC = true; + public static final IndexingMode INDEXING_POLICY_MODE = IndexingMode.CONSISTENT; + public static final String INCLUDED_PATH_0 = "/*"; + public static final String INCLUDED_PATH_1 = "/cache/*"; + public static final String INCLUDED_PATH_2 = "/entities/*"; + public static final String[] INCLUDED_PATHS = { + INCLUDED_PATH_0, + INCLUDED_PATH_1, + INCLUDED_PATH_2, + }; + public static final String EXCLUDED_PATH_0 = "/excluded/*"; + public static final String EXCLUDED_PATH_1 = "/props/*"; + public static final String EXCLUDED_PATH_2 = "/_etag/?"; + public static final String[] EXCLUDED_PATHS = { + EXCLUDED_PATH_0, + EXCLUDED_PATH_1, + EXCLUDED_PATH_2, + }; + + public static final String DB_NAME = "testdb"; + public static final String FIRST_NAME = "first_name_li"; + public static final String PATCH_FIRST_NAME = "first_name_replace"; + public static final String LAST_NAME = "last_name_p"; + public static final Integer ZIP_CODE = 12345; + 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 Integer NEW_ZIP_CODE = 67890; + 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 LEVEL_2 = "C"; + 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 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 PROPERTY_ID = "id"; + public static final String PROPERTY_ZIP_CODE = "zipCode"; + + 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"; + + public static final String COURSE_NAME = "test-course"; + public static final String DEPARTMENT = "test-department"; + + public static final Integer AGE = 24; + public static final Integer PATCH_AGE_1 = 25; + public static final Integer PATCH_AGE_INCREMENT = 2; + + public static final Map PASSPORT_IDS_BY_COUNTRY = new HashMap() {{ + put("United States of America", "123456789"); + put("Côte d'Ivoire", "IC1234567"); + }}; + + public static final Map NEW_PASSPORT_IDS_BY_COUNTRY = new HashMap() {{ + put("United Kingdom", "123456789"); + put("Germany", "IC1234567"); + }}; + + private TestConstants() { + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestUtils.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestUtils.java new file mode 100644 index 000000000000..5f3572b7ddc0 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/common/TestUtils.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/config/AbstractCosmosConfigurationIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/config/AbstractCosmosConfigurationIT.java new file mode 100644 index 000000000000..a1930b3df85f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/config/AbstractCosmosConfigurationIT.java @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.config; + +import com.azure.cosmos.ConsistencyLevel; +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.common.ExpressionResolver; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +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 java.lang.reflect.Field; + +import static org.junit.Assert.assertNotNull; + +public class AbstractCosmosConfigurationIT { + + @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 containsCosmosFactory() { + final AbstractApplicationContext context = new AnnotationConfigApplicationContext( + TestCosmosConfiguration.class); + + Assertions.assertThat(context.getBean(CosmosFactory.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(Constants.OBJECT_MAPPER_BEAN_NAME)).isNotNull(); + } + + @Test + public void testCosmosClientBuilderConfigurable() throws IllegalAccessException { + final AbstractApplicationContext context = new AnnotationConfigApplicationContext( + RequestOptionsConfiguration.class); + final CosmosFactory factory = context.getBean(CosmosFactory.class); + + Assertions.assertThat(factory).isNotNull(); + + final CosmosAsyncClient cosmosAsyncClient = factory.getCosmosAsyncClient(); + + Assertions.assertThat(cosmosAsyncClient).isNotNull(); + final Field consistencyLevelField = FieldUtils.getDeclaredField(CosmosAsyncClient.class, + "desiredConsistencyLevel", true); + ConsistencyLevel consistencyLevel = + (ConsistencyLevel) consistencyLevelField.get(cosmosAsyncClient); + Assertions.assertThat(consistencyLevel).isEqualTo(ConsistencyLevel.CONSISTENT_PREFIX); + } + + @Configuration + @PropertySource(value = { "classpath:application.properties" }) + static class TestCosmosConfiguration extends AbstractCosmosConfiguration { + + @Value("${cosmos.uri:}") + private String cosmosDbUri; + + @Value("${cosmos.key:}") + private String cosmosDbKey; + + @Value("${cosmosdb.database:}") + private String database; + + @Bean + public CosmosClientBuilder getCosmosClientBuilder() { + return new CosmosClientBuilder() + .endpoint(cosmosDbUri) + .key(cosmosDbKey); + } + + @Override + protected String getDatabaseName() { + return StringUtils.hasText(this.database) ? this.database : TestConstants.DB_NAME; + } + } + + @Configuration + static class ObjectMapperConfiguration extends TestCosmosConfiguration { + @Bean(name = Constants.OBJECT_MAPPER_BEAN_NAME) + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + } + + @Configuration + @PropertySource(value = { "classpath:application.properties" }) + static class RequestOptionsConfiguration extends AbstractCosmosConfiguration { + + @Value("${cosmos.uri:}") + private String cosmosDbUri; + + @Value("${cosmos.key:}") + private String cosmosDbKey; + + @Value("${cosmosdb.database:}") + private String database; + + @Bean + public CosmosClientBuilder getCosmosClientBuilder() { + return new CosmosClientBuilder() + .key(cosmosDbKey) + .endpoint(cosmosDbUri) + .consistencyLevel(ConsistencyLevel.CONSISTENT_PREFIX); + } + + @Override + protected String getDatabaseName() { + return StringUtils.hasText(this.database) ? this.database : TestConstants.DB_NAME; + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIT.java new file mode 100644 index 000000000000..bb69ea3ad83e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIT.java @@ -0,0 +1,1045 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosAsyncDatabase; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.CosmosException; +import com.azure.cosmos.implementation.ConflictException; +import com.azure.cosmos.implementation.PreconditionFailedException; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.cosmos.models.ThroughputResponse; +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.PageTestUtils; +import com.azure.spring.data.cosmos.common.PropertyLoader; +import com.azure.spring.data.cosmos.common.ResponseDiagnosticsTestUtils; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.generator.FindQuerySpecGenerator; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.domain.AuditableEntity; +import com.azure.spring.data.cosmos.domain.AutoScaleSample; +import com.azure.spring.data.cosmos.domain.GenIdEntity; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.ClassRule; +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.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static com.azure.spring.data.cosmos.common.TestConstants.ADDRESSES; +import static com.azure.spring.data.cosmos.common.TestConstants.AGE; +import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBY1; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_1; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_2; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_3; +import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_PASSPORT_IDS_BY_COUNTRY; +import static com.azure.spring.data.cosmos.common.TestConstants.NOT_EXIST_ID; +import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_1; +import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_2; +import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_3; +import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_1; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_INCREMENT; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBY1; +import static com.azure.spring.data.cosmos.common.TestConstants.UPDATED_FIRST_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +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, AGE, PASSPORT_IDS_BY_COUNTRY); + + private static final Person TEST_PERSON_2 = new Person(ID_2, NEW_FIRST_NAME, NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + private static final Person TEST_PERSON_3 = new Person(ID_3, NEW_FIRST_NAME, NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + private static final String PRECONDITION_IS_NOT_MET = "is not met"; + + private static final String WRONG_ETAG = "WRONG_ETAG"; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final JsonNode NEW_PASSPORT_IDS_BY_COUNTRY_JSON = OBJECT_MAPPER.convertValue(NEW_PASSPORT_IDS_BY_COUNTRY, JsonNode.class); + + private static final CosmosPatchOperations operations = CosmosPatchOperations + .create() + .replace("/age", PATCH_AGE_1); + + CosmosPatchOperations multiPatchOperations = CosmosPatchOperations + .create() + .set("/firstName", PATCH_FIRST_NAME) + .replace("/passportIdsByCountry", NEW_PASSPORT_IDS_BY_COUNTRY_JSON) + .add("/hobbies/2", PATCH_HOBBY1) + .remove("/shippingAddresses/1") + .increment("/age", PATCH_AGE_INCREMENT); + + private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions(); + + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + private static CosmosAsyncClient client; + private static CosmosTemplate cosmosTemplate; + private static CosmosEntityInformation personInfo; + private static String containerName; + + private Person insertedPerson; + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosClientBuilder cosmosClientBuilder; + @Autowired + private CosmosConfig cosmosConfig; + @Autowired + private ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils; + + public CosmosTemplateIT() throws JsonProcessingException { + } + + @Before + public void setUp() throws ClassNotFoundException { + if (cosmosTemplate == null) { + client = CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + personInfo = new CosmosEntityInformation<>(Person.class); + containerName = personInfo.getContainerName(); + cosmosTemplate = createCosmosTemplate(cosmosConfig, TestConstants.DB_NAME); + } + + collectionManager.ensureContainersCreatedAndEmpty(cosmosTemplate, Person.class, + GenIdEntity.class, AuditableEntity.class); + insertedPerson = cosmosTemplate.insert(Person.class.getSimpleName(), TEST_PERSON, + new PartitionKey(TEST_PERSON.getLastName())); + } + + private CosmosTemplate createCosmosTemplate(CosmosConfig config, String dbName) throws ClassNotFoundException { + final CosmosFactory cosmosFactory = new CosmosFactory(client, dbName); + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + final MappingCosmosConverter cosmosConverter = new MappingCosmosConverter(mappingContext, null); + return new CosmosTemplate(cosmosFactory, config, cosmosConverter); + } + + private void insertPerson(Person person) { + cosmosTemplate.insert(person, + new PartitionKey(personInfo.getPartitionKeyFieldValue(person))); + } + + @Test + public void testInsertDuplicateIdShouldFailWithConflictException() { + try { + cosmosTemplate.insert(Person.class.getSimpleName(), TEST_PERSON, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + fail(); + } catch (CosmosAccessException ex) { + assertThat(ex.getCosmosException()).isInstanceOf(ConflictException.class); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + } + + @Test(expected = CosmosAccessException.class) + public void testInsertShouldFailIfColumnNotAnnotatedWithAutoGenerate() { + final Person person = new Person(null, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + cosmosTemplate.insert(Person.class.getSimpleName(), person, new PartitionKey(person.getLastName())); + } + + @Test + public void testInsertShouldGenerateIdIfColumnAnnotatedWithAutoGenerate() { + final GenIdEntity entity = new GenIdEntity(null, "foo"); + final GenIdEntity insertedEntity = cosmosTemplate.insert(GenIdEntity.class.getSimpleName(), + entity, null); + assertThat(insertedEntity.getId()).isNotNull(); + } + + @Test + public void testFindAll() { + final List result = TestUtils.toList(cosmosTemplate.findAll(Person.class.getSimpleName(), + Person.class)); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0)).isEqualTo(TEST_PERSON); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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.getCosmosDiagnostics()).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(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @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 = TestUtils.toList(cosmosTemplate.findByIds(ids, Person.class, containerName)); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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(); + final Person newPerson = new Person(TEST_PERSON.getId(), firstName, NEW_FIRST_NAME, null, null, + AGE, PASSPORT_IDS_BY_COUNTRY); + + final Person person = cosmosTemplate.upsertAndReturnEntity(Person.class.getSimpleName(), newPerson); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + 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(), + AGE, PASSPORT_IDS_BY_COUNTRY); + updated.set_etag(insertedPerson.get_etag()); + + final Person updatedPerson = cosmosTemplate.upsertAndReturnEntity(Person.class.getSimpleName(), updated); + + 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(), + AGE, PASSPORT_IDS_BY_COUNTRY); + updated.set_etag(insertedPerson.get_etag()); + + final Person person = cosmosTemplate.upsertAndReturnEntity(Person.class.getSimpleName(), updated); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + assertEquals(person, updated); + } + + @Test + public void testPatch() { + Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations); + assertEquals(patchedPerson.getAge(), PATCH_AGE_1); + } + + @Test + public void testPatchMultiOperations() { + Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, multiPatchOperations); + assertEquals(patchedPerson.getAge().intValue(), (AGE + PATCH_AGE_INCREMENT)); + assertEquals(patchedPerson.getHobbies(), PATCH_HOBBIES); + assertEquals(patchedPerson.getFirstName(), PATCH_FIRST_NAME); + assertEquals(patchedPerson.getShippingAddresses().size(), 1); + assertEquals(patchedPerson.getPassportIdsByCountry(), NEW_PASSPORT_IDS_BY_COUNTRY); + } + + @Test + public void testPatchPreConditionSuccess() { + options.setFilterPredicate("FROM person p WHERE p.lastName = '"+LAST_NAME+"'"); + Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options); + assertEquals(patchedPerson.getAge(), PATCH_AGE_1); + } + + @Test + public void testPatchPreConditionFail() { + try { + options.setFilterPredicate("FROM person p WHERE p.lastName = 'dummy'"); + Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options); + assertEquals(patchedPerson.getAge(), PATCH_AGE_1); + fail(); + } catch (CosmosAccessException ex) { + assertThat(ex.getCosmosException()).isInstanceOf(PreconditionFailedException.class); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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(), + AGE, PASSPORT_IDS_BY_COUNTRY); + updated.set_etag(WRONG_ETAG); + + try { + cosmosTemplate.upsert(Person.class.getSimpleName(), updated); + } catch (CosmosAccessException e) { + assertThat(e.getCosmosException()).isNotNull(); + final Throwable cosmosClientException = e.getCosmosException(); + assertThat(cosmosClientException).isInstanceOf(CosmosException.class); + assertThat(cosmosClientException.getMessage()).contains(PRECONDITION_IS_NOT_MET); + assertThat(responseDiagnosticsTestUtils.getDiagnostics()).isNotNull(); + + 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.count(Person.class.getSimpleName())).isEqualTo(2); + + cosmosTemplate.deleteById(Person.class.getSimpleName(), TEST_PERSON.getId(), + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final List result = TestUtils.toList(cosmosTemplate.findAll(Person.class)); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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 testDeleteByEntity() { + Person insertedPerson = cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getLastName())); + assertThat(cosmosTemplate.count(Person.class.getSimpleName())).isEqualTo(2); + + cosmosTemplate.deleteEntity(Person.class.getSimpleName(), insertedPerson); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final List result = TestUtils.toList(cosmosTemplate.findAll(Person.class)); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + assertThat(result.size()).isEqualTo(1); + assertEquals(result.get(0), TEST_PERSON); + } + + @Test + public void testCountByContainer() { + final long prevCount = cosmosTemplate.count(containerName); + assertThat(prevCount).isEqualTo(1); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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.getCosmosDiagnostics()).isNotNull(); + + final long newCount = cosmosTemplate.count(containerName); + assertThat(newCount).isEqualTo(2); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON_2.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + + final long count = cosmosTemplate.count(query, containerName); + assertThat(count).isEqualTo(1); + + // add ignoreCase testing + final Criteria criteriaIgnoreCase = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON_2.getFirstName().toUpperCase()), Part.IgnoreCaseType.ALWAYS); + final CosmosQuery queryIgnoreCase = new CosmosQuery(criteriaIgnoreCase); + + final long countIgnoreCase = cosmosTemplate.count(queryIgnoreCase, containerName); + assertThat(countIgnoreCase).isEqualTo(1); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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.getCosmosDiagnostics()).isNotNull(); + 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); + PageTestUtils.validateNonLastPage(page1, PAGE_SIZE_1); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final Page page2 = cosmosTemplate.findAll(page1.nextPageable(), Person.class, + containerName); + assertThat(page2.getContent().size()).isEqualTo(1); + PageTestUtils.validateLastPage(page2, PAGE_SIZE_1); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindAllPageableMultiPagesPageSizeTwo() { + 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.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_2, null); + final Page page1 = cosmosTemplate.findAll(pageRequest, Person.class, containerName); + + final List resultPage1 = TestUtils.toList(page1); + final List expected = Lists.newArrayList(TEST_PERSON, TEST_PERSON_2); + assertThat(resultPage1.size()).isEqualTo(expected.size()); + assertThat(resultPage1).containsAll(expected); + PageTestUtils.validateNonLastPage(page1, PAGE_SIZE_2); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final Page page2 = cosmosTemplate.findAll(page1.nextPageable(), Person.class, containerName); + + final List resultPage2 = TestUtils.toList(page2); + final List expected2 = Lists.newArrayList(TEST_PERSON_3); + assertThat(resultPage2.size()).isEqualTo(expected2.size()); + assertThat(resultPage2).containsAll(expected2); + PageTestUtils.validateLastPage(page2, PAGE_SIZE_2); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME), Part.IgnoreCaseType.NEVER); + final PageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_2, null); + final CosmosQuery query = new CosmosQuery(criteria).with(pageRequest); + + final Page page = cosmosTemplate.paginationQuery(query, Person.class, containerName); + assertThat(page.getContent().size()).isEqualTo(1); + PageTestUtils.validateLastPage(page, PAGE_SIZE_2); + + // add ignore case testing + final Criteria criteriaIgnoreCase = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME.toUpperCase()), Part.IgnoreCaseType.ALWAYS); + final CosmosQuery queryIgnoreCase = new CosmosQuery(criteriaIgnoreCase).with(pageRequest); + + final Page pageIgnoreCase = cosmosTemplate.paginationQuery(queryIgnoreCase, Person.class, + containerName); + assertThat(pageIgnoreCase.getContent().size()).isEqualTo(1); + PageTestUtils.validateLastPage(pageIgnoreCase, PAGE_SIZE_2); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindWithSortAndLimit() { + final Person testPerson4 = new Person("id_4", "fred", NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + final Person testPerson5 = new Person("id_5", "barney", NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + final Person testPerson6 = new Person("id_6", "george", NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + insertPerson(testPerson4); + insertPerson(testPerson5); + insertPerson(testPerson6); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "lastName", + Collections.singletonList(NEW_LAST_NAME), Part.IgnoreCaseType.ALWAYS); + final CosmosQuery query = new CosmosQuery(criteria); + query.with(Sort.by(Sort.Direction.ASC, "firstName")); + + final List result = TestUtils.toList(cosmosTemplate.find(query, Person.class, containerName)); + assertThat(result.size()).isEqualTo(3); + assertThat(result.get(0).getFirstName()).isEqualTo("barney"); + assertThat(result.get(1).getFirstName()).isEqualTo("fred"); + assertThat(result.get(2).getFirstName()).isEqualTo("george"); + + query.withLimit(1); + final List resultWithLimit = TestUtils.toList(cosmosTemplate.find(query, Person.class, containerName)); + assertThat(resultWithLimit.size()).isEqualTo(1); + assertThat(resultWithLimit.get(0).getFirstName()).isEqualTo("barney"); + } + + @Test + public void testFindWithOffsetAndLimit() { + final Person testPerson4 = new Person("id_4", "fred", NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + final Person testPerson5 = new Person("id_5", "barney", NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + final Person testPerson6 = new Person("id_6", "george", NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + insertPerson(testPerson4); + insertPerson(testPerson5); + insertPerson(testPerson6); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "lastName", + Collections.singletonList(NEW_LAST_NAME), Part.IgnoreCaseType.ALWAYS); + final CosmosQuery query = new CosmosQuery(criteria); + query.with(Sort.by(Sort.Direction.ASC, "firstName")); + + final List result = TestUtils.toList(cosmosTemplate.find(query, Person.class, containerName)); + assertThat(result.size()).isEqualTo(3); + assertThat(result.get(0).getFirstName()).isEqualTo("barney"); + assertThat(result.get(1).getFirstName()).isEqualTo("fred"); + assertThat(result.get(2).getFirstName()).isEqualTo("george"); + + query.withOffsetAndLimit(1, 1); + final List resultWithLimit = TestUtils.toList(cosmosTemplate.find(query, Person.class, containerName)); + assertThat(resultWithLimit.size()).isEqualTo(1); + assertThat(resultWithLimit.get(0).getFirstName()).isEqualTo("fred"); + } + + @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.getCosmosDiagnostics()).isNotNull(); + 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); + PageTestUtils.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.getCosmosDiagnostics()).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, AGE, PASSPORT_IDS_BY_COUNTRY); + final Person testPerson5 = new Person("id_5", "fred", NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + 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); + PageTestUtils.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.nextPageable(), Person.class, + containerName); + + assertThat(secondPage.getContent().size()).isEqualTo(2); + PageTestUtils.validateLastPage(secondPage, PAGE_SIZE_3); + + final List secondPageResults = secondPage.getContent(); + assertThat(secondPageResults.get(0).getFirstName()).isEqualTo(NEW_FIRST_NAME); + assertThat(secondPageResults.get(1).getFirstName()).isEqualTo(NEW_FIRST_NAME); + } + + @Test + public void testExists() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + final Boolean exists = cosmosTemplate.exists(query, Person.class, containerName); + assertThat(exists).isTrue(); + + // add ignore testing + final Criteria criteriaIgnoreCase = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName().toUpperCase()), Part.IgnoreCaseType.ALWAYS); + final CosmosQuery queryIgnoreCase = new CosmosQuery(criteriaIgnoreCase); + final Boolean existsIgnoreCase = cosmosTemplate.exists(queryIgnoreCase, Person.class, containerName); + assertThat(existsIgnoreCase).isTrue(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testArrayContainsCriteria() { + Criteria hasHobby = Criteria.getInstance(CriteriaType.ARRAY_CONTAINS, "hobbies", + Collections.singletonList(HOBBY1), Part.IgnoreCaseType.NEVER); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(hasHobby), Person.class, + containerName)); + + assertThat(people).containsExactly(TEST_PERSON); + } + + @Test + public void testContainsCriteria() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + cosmosTemplate.insert(TEST_PERSON_3, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3))); + Person TEST_PERSON_4 = new Person("id-4", "NEW_FIRST_NAME", NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + cosmosTemplate.insert(TEST_PERSON_4, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_4))); + + Criteria containsCaseSensitive = Criteria.getInstance(CriteriaType.CONTAINING, "firstName", + Collections.singletonList("first"), Part.IgnoreCaseType.NEVER); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(containsCaseSensitive), Person.class, + containerName)); + assertThat(people).containsExactly(TEST_PERSON, TEST_PERSON_2, TEST_PERSON_3); + + Criteria containsNotCaseSensitive = Criteria.getInstance(CriteriaType.CONTAINING, "firstName", + Collections.singletonList("first"), Part.IgnoreCaseType.ALWAYS); + List people2 = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(containsNotCaseSensitive), Person.class, + containerName)); + assertThat(people2).containsExactly(TEST_PERSON, TEST_PERSON_2, TEST_PERSON_3, TEST_PERSON_4); + } + + @Test + public void testContainsCriteria2() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + cosmosTemplate.insert(TEST_PERSON_3, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3))); + + Criteria containsCaseSensitive = Criteria.getInstance(CriteriaType.CONTAINING, "id", + Collections.singletonList("1"), Part.IgnoreCaseType.NEVER); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(containsCaseSensitive), Person.class, + containerName)); + assertThat(people).containsExactly(TEST_PERSON); + + Criteria containsCaseSensitive2 = Criteria.getInstance(CriteriaType.CONTAINING, "id", + Collections.singletonList("2"), Part.IgnoreCaseType.NEVER); + List people2 = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(containsCaseSensitive2), Person.class, + containerName)); + assertThat(people2).containsExactly(TEST_PERSON_2); + + Criteria containsCaseSensitive3 = Criteria.getInstance(CriteriaType.CONTAINING, "id", + Collections.singletonList("3"), Part.IgnoreCaseType.NEVER); + List people3 = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(containsCaseSensitive3), Person.class, + containerName)); + assertThat(people3).containsExactly(TEST_PERSON_3); + } + + @Test + public void testNotContainsCriteria() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + cosmosTemplate.insert(TEST_PERSON_3, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3))); + Person TEST_PERSON_4 = new Person("id-4", "NEW_FIRST_NAME", NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + cosmosTemplate.insert(TEST_PERSON_4, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_4))); + + Criteria notContainsCaseSensitive = Criteria.getInstance(CriteriaType.NOT_CONTAINING, "firstName", + Collections.singletonList("li"), Part.IgnoreCaseType.NEVER); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(notContainsCaseSensitive), Person.class, + containerName)); + assertThat(people).containsExactly(TEST_PERSON_2, TEST_PERSON_3, TEST_PERSON_4); + + Criteria notContainsNotCaseSensitive = Criteria.getInstance(CriteriaType.NOT_CONTAINING, "firstName", + Collections.singletonList("new"), Part.IgnoreCaseType.ALWAYS); + List people2 = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(notContainsNotCaseSensitive), Person.class, + containerName)); + assertThat(people2).containsExactly(TEST_PERSON); + } + + @Test + public void testNotContainsCriteria2() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + cosmosTemplate.insert(TEST_PERSON_3, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3))); + + Criteria notContainsCaseSensitive = Criteria.getInstance(CriteriaType.NOT_CONTAINING, "id", + Collections.singletonList("1"), Part.IgnoreCaseType.NEVER); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(notContainsCaseSensitive), Person.class, + containerName)); + assertThat(people).containsExactly(TEST_PERSON_2, TEST_PERSON_3); + + Criteria notContainsCaseSensitive2 = Criteria.getInstance(CriteriaType.NOT_CONTAINING, "id", + Collections.singletonList("2"), Part.IgnoreCaseType.NEVER); + List people2 = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(notContainsCaseSensitive2), Person.class, + containerName)); + assertThat(people2).containsExactly(TEST_PERSON, TEST_PERSON_3); + + Criteria notContainsCaseSensitive3 = Criteria.getInstance(CriteriaType.NOT_CONTAINING, "id", + Collections.singletonList("3"), Part.IgnoreCaseType.NEVER); + List people3 = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(notContainsCaseSensitive3), Person.class, + containerName)); + assertThat(people3).containsExactly(TEST_PERSON, TEST_PERSON_2); + } + + @Test + public void testIsNotNullCriteriaCaseSensitive() { + Criteria hasLastName = Criteria.getInstance(CriteriaType.IS_NOT_NULL, "lastName", + Collections.emptyList(), + Part.IgnoreCaseType.ALWAYS); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(hasLastName), Person.class, + containerName)); + + assertThat(people).containsExactly(TEST_PERSON); + } + + @Test + public void testStartsWithCriteriaCaseSensitive() { + Criteria nameStartsWith = Criteria.getInstance(CriteriaType.STARTS_WITH, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName().toUpperCase()), + Part.IgnoreCaseType.ALWAYS); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(nameStartsWith), Person.class, + containerName)); + + assertThat(people).containsExactly(TEST_PERSON); + } + + @Test + public void testIsEqualCriteriaCaseSensitive() { + Criteria nameStartsWith = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName().toUpperCase()), + Part.IgnoreCaseType.ALWAYS); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(nameStartsWith), Person.class, + containerName)); + + assertThat(people).containsExactly(TEST_PERSON); + } + + @Test + public void testStringEqualsCriteriaCaseSensitive() { + Criteria nameStartsWith = Criteria.getInstance(CriteriaType.STRING_EQUALS, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName().toUpperCase()), + Part.IgnoreCaseType.ALWAYS); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(nameStartsWith), Person.class, + containerName)); + + assertThat(people).containsExactly(TEST_PERSON); + } + + @Test + public void testBetweenCriteria() { + Criteria ageBetween = Criteria.getInstance(CriteriaType.BETWEEN, "age", Arrays.asList(AGE - 1, AGE + 1), + Part.IgnoreCaseType.NEVER); + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(ageBetween), Person.class, + containerName)); + assertThat(people).containsExactly(TEST_PERSON); + } + + @Test + public void testFindWithEqualCriteriaContainingNestedProperty() { + String postalCode = ADDRESSES.get(0).getPostalCode(); + String subjectWithNestedProperty = "shippingAddresses[0]['postalCode']"; + Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, subjectWithNestedProperty, + Collections.singletonList(postalCode), Part.IgnoreCaseType.NEVER); + + List people = TestUtils.toList(cosmosTemplate.find(new CosmosQuery(criteria), Person.class, + containerName)); + + assertThat(people).containsExactly(TEST_PERSON); + } + + @Test + public void testRunQueryWithEqualCriteriaContainingSpaces() { + String usaPassportId = PASSPORT_IDS_BY_COUNTRY.get("United States of America"); + String subjectWithSpaces = "passportIdsByCountry['United States of America']"; + Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, subjectWithSpaces, + Collections.singletonList(usaPassportId), Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(criteria)); + + List people = TestUtils.toList(cosmosTemplate.runQuery(sqlQuerySpec, Person.class, Person.class)); + + assertThat(people).containsExactly(TEST_PERSON); + } + + @Test + public void testRunQueryWithSimpleReturnType() { + Criteria ageBetween = Criteria.getInstance(CriteriaType.BETWEEN, "age", Arrays.asList(AGE - 1, AGE + 1), + Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(ageBetween)); + List people = TestUtils.toList(cosmosTemplate.runQuery(sqlQuerySpec, Person.class, Person.class)); + assertThat(people).containsExactly(TEST_PERSON); + } + + @Test + public void testSliceQuery() { + cosmosTemplate.insert(TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME), Part.IgnoreCaseType.NEVER); + final PageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_2, null); + final CosmosQuery query = new CosmosQuery(criteria).with(pageRequest); + + final Slice slice = cosmosTemplate.sliceQuery(query, Person.class, containerName); + assertThat(slice.getContent().size()).isEqualTo(1); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + @Test + public void testRunSliceQuery() { + cosmosTemplate.insert(TEST_PERSON_2, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME), Part.IgnoreCaseType.NEVER); + final PageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_2, null); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(criteria)); + final Slice slice = cosmosTemplate.runSliceQuery(sqlQuerySpec, pageRequest, Person.class, Person.class); + assertThat(slice.getContent().size()).isEqualTo(1); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void createWithAutoscale() throws ClassNotFoundException { + final CosmosEntityInformation autoScaleSampleInfo = + new CosmosEntityInformation<>(AutoScaleSample.class); + CosmosContainerProperties containerProperties = cosmosTemplate.createContainerIfNotExists(autoScaleSampleInfo); + assertNotNull(containerProperties); + ThroughputResponse throughput = client.getDatabase(TestConstants.DB_NAME) + .getContainer(autoScaleSampleInfo.getContainerName()) + .readThroughput() + .block(); + assertNotNull(throughput); + assertEquals(Integer.parseInt(TestConstants.AUTOSCALE_MAX_THROUGHPUT), + throughput.getProperties().getAutoscaleMaxThroughput()); + } + + @Test + public void createDatabaseWithThroughput() throws ClassNotFoundException { + final String configuredThroughputDbName = TestConstants.DB_NAME + "-configured-throughput"; + deleteDatabaseIfExists(configuredThroughputDbName); + + Integer expectedRequestUnits = 700; + final CosmosConfig config = CosmosConfig.builder() + .enableDatabaseThroughput(false, expectedRequestUnits) + .build(); + final CosmosTemplate configuredThroughputCosmosTemplate = createCosmosTemplate(config, configuredThroughputDbName); + + final CosmosEntityInformation personInfo = + new CosmosEntityInformation<>(Person.class); + configuredThroughputCosmosTemplate.createContainerIfNotExists(personInfo); + + final CosmosAsyncDatabase database = client.getDatabase(configuredThroughputDbName); + final ThroughputResponse response = database.readThroughput().block(); + assertEquals(expectedRequestUnits, response.getProperties().getManualThroughput()); + } + + @Test + public void queryWithMaxDegreeOfParallelism() throws ClassNotFoundException { + final CosmosConfig config = CosmosConfig.builder() + .maxDegreeOfParallelism(20) + .build(); + final CosmosTemplate maxDegreeOfParallelismCosmosTemplate = createCosmosTemplate(config, TestConstants.DB_NAME); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + + final long count = maxDegreeOfParallelismCosmosTemplate.count(query, containerName); + + assertEquals((int) ReflectionTestUtils.getField(maxDegreeOfParallelismCosmosTemplate, "maxDegreeOfParallelism"), 20); + } + + @Test + public void queryWithMaxBufferedItemCount() throws ClassNotFoundException { + final CosmosConfig config = CosmosConfig.builder() + .maxBufferedItemCount(500) + .build(); + final CosmosTemplate maxBufferedItemCountCosmosTemplate = createCosmosTemplate(config, TestConstants.DB_NAME); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + + final long count = maxBufferedItemCountCosmosTemplate.count(query, containerName); + + assertEquals((int) ReflectionTestUtils.getField(maxBufferedItemCountCosmosTemplate, "maxBufferedItemCount"), 500); + } + + @Test + public void queryWithResponseContinuationTokenLimitInKb() throws ClassNotFoundException { + final CosmosConfig config = CosmosConfig.builder() + .responseContinuationTokenLimitInKb(2000) + .build(); + final CosmosTemplate responseContinuationTokenLimitInKbCosmosTemplate = + createCosmosTemplate(config, TestConstants.DB_NAME); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + + final long count = responseContinuationTokenLimitInKbCosmosTemplate.count(query, containerName); + + assertEquals((int) ReflectionTestUtils.getField(responseContinuationTokenLimitInKbCosmosTemplate, + "responseContinuationTokenLimitInKb"), 2000); + } + + @Test + public void queryDatabaseWithQueryMerticsEnabled() throws ClassNotFoundException { + final CosmosConfig config = CosmosConfig.builder() + .enableQueryMetrics(true) + .build(); + final CosmosTemplate queryMetricsEnabledCosmosTemplate = createCosmosTemplate(config, TestConstants.DB_NAME); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + + final long count = queryMetricsEnabledCosmosTemplate.count(query, containerName); + + assertEquals((boolean) ReflectionTestUtils.getField(queryMetricsEnabledCosmosTemplate, "queryMetricsEnabled"), true); + } + + @Test + public void userAgentSpringDataCosmosSuffix() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + // getUserAgentSuffix method from CosmosClientBuilder + Method getUserAgentSuffix = CosmosClientBuilder.class.getDeclaredMethod("getUserAgentSuffix"); + getUserAgentSuffix.setAccessible(true); + String userAgentSuffix = (String) getUserAgentSuffix.invoke(cosmosClientBuilder); + assertThat(userAgentSuffix).contains(Constants.USER_AGENT_SUFFIX); + assertThat(userAgentSuffix).contains(PropertyLoader.getProjectVersion()); + } + + private void deleteDatabaseIfExists(String dbName) { + CosmosAsyncDatabase database = client.getDatabase(dbName); + try { + database.delete().block(); + } catch (CosmosException ex) { + assertEquals(ex.getStatusCode(), 404); + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplatePartitionIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplatePartitionIT.java new file mode 100644 index 000000000000..c09338fef82d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplatePartitionIT.java @@ -0,0 +1,428 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosBridgeInternal; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.implementation.AsyncDocumentClient; +import com.azure.cosmos.implementation.query.PartitionedQueryExecutionInfo; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.PageTestUtils; +import com.azure.spring.data.cosmos.common.ResponseDiagnosticsTestUtils; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.generator.FindQuerySpecGenerator; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.domain.PartitionPerson; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Before; +import org.junit.ClassRule; +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.data.repository.query.parser.Part; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.azure.spring.data.cosmos.common.TestConstants.ADDRESSES; +import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_1; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_2; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_3; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_ZIP_CODE; +import static com.azure.spring.data.cosmos.common.TestConstants.NOT_EXIST_ID; +import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_1; +import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_2; +import static com.azure.spring.data.cosmos.common.TestConstants.PROPERTY_ID; +import static com.azure.spring.data.cosmos.common.TestConstants.PROPERTY_ZIP_CODE; +import static com.azure.spring.data.cosmos.common.TestConstants.UPDATED_FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.ZIP_CODE; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.springframework.data.domain.Sort.Direction.ASC; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CosmosTemplatePartitionIT { + private static final PartitionPerson TEST_PERSON = new PartitionPerson(ID_1, FIRST_NAME, ZIP_CODE, + HOBBIES, ADDRESSES); + + private static final PartitionPerson TEST_PERSON_2 = new PartitionPerson(ID_2, NEW_FIRST_NAME, + NEW_ZIP_CODE, HOBBIES, ADDRESSES); + + private static final PartitionPerson TEST_PERSON_3 = new PartitionPerson(ID_3, FIRST_NAME, NEW_ZIP_CODE, + HOBBIES, ADDRESSES); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + private static CosmosFactory cosmosFactory; + private static CosmosTemplate cosmosTemplate; + private static String containerName; + private static CosmosEntityInformation personInfo; + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosConfig cosmosConfig; + @Autowired + private CosmosClientBuilder cosmosClientBuilder; + @Autowired + private ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils; + + @Before + public void setUp() throws ClassNotFoundException { + if (cosmosTemplate == null) { + // Query plan caching is enabled by default + CosmosAsyncClient client = CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + cosmosFactory = new CosmosFactory(client, TestConstants.DB_NAME); + 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 CosmosTemplate(cosmosFactory, cosmosConfig, dbConverter); + } + + collectionManager.ensureContainersCreatedAndEmpty(cosmosTemplate, PartitionPerson.class); + cosmosTemplate.insert(PartitionPerson.class.getSimpleName(), TEST_PERSON, + new PartitionKey(TEST_PERSON.getZipCode())); + } + + @Test + public void testFindWithPartition() { + Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, PROPERTY_ZIP_CODE, + Collections.singletonList(ZIP_CODE), Part.IgnoreCaseType.NEVER); + CosmosQuery query = new CosmosQuery(criteria); + List result = TestUtils.toList(cosmosTemplate.find(query, PartitionPerson.class, + PartitionPerson.class.getSimpleName())); + + assertThat(result.size()).isEqualTo(1); + assertEquals(TEST_PERSON, result.get(0)); + + criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, PROPERTY_ID, + Collections.singletonList(ID_1), Part.IgnoreCaseType.NEVER); + query = new CosmosQuery(criteria); + result = TestUtils.toList(cosmosTemplate.find(query, PartitionPerson.class, + PartitionPerson.class.getSimpleName())); + + assertThat(result.size()).isEqualTo(1); + assertEquals(TEST_PERSON, result.get(0)); + } + + @Test + public void testFindWithPartitionWithQueryPlanCachingEnabled() { + Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, PROPERTY_ZIP_CODE, + Collections.singletonList(ZIP_CODE), Part.IgnoreCaseType.NEVER); + CosmosQuery query = new CosmosQuery(criteria); + SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + List result = TestUtils.toList(cosmosTemplate.find(query, PartitionPerson.class, + PartitionPerson.class.getSimpleName())); + + assertThat(result.size()).isEqualTo(1); + assertEquals(TEST_PERSON, result.get(0)); + + CosmosAsyncClient cosmosAsyncClient = cosmosFactory.getCosmosAsyncClient(); + AsyncDocumentClient asyncDocumentClient = CosmosBridgeInternal.getAsyncDocumentClient(cosmosAsyncClient); + Map initialCache = asyncDocumentClient.getQueryPlanCache(); + assertThat(initialCache.containsKey(sqlQuerySpec.getQueryText())).isTrue(); + int initialSize = initialCache.size(); + + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())); + + criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, PROPERTY_ZIP_CODE, + Collections.singletonList(NEW_ZIP_CODE), Part.IgnoreCaseType.NEVER); + query = new CosmosQuery(criteria); + // Fire the same query but with different partition key value to make sure query plan caching is enabled + sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + result = TestUtils.toList(cosmosTemplate.find(query, PartitionPerson.class, + PartitionPerson.class.getSimpleName())); + + Map postQueryCallCache = asyncDocumentClient.getQueryPlanCache(); + assertThat(postQueryCallCache.containsKey(sqlQuerySpec.getQueryText())).isTrue(); + assertThat(postQueryCallCache.size()).isEqualTo(initialSize); + assertThat(result.size()).isEqualTo(1); + assertEquals(TEST_PERSON_2, result.get(0)); + } + + @Test + public void testFindIgnoreCaseWithPartition() { + Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, PROPERTY_ZIP_CODE, + Collections.singletonList(ZIP_CODE), Part.IgnoreCaseType.NEVER); + CosmosQuery query = new CosmosQuery(criteria); + List result = TestUtils.toList(cosmosTemplate.find(query, PartitionPerson.class, + PartitionPerson.class.getSimpleName())); + + assertThat(result.size()).isEqualTo(1); + assertEquals(TEST_PERSON, result.get(0)); + + criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, PROPERTY_ID, + Collections.singletonList(ID_1.toUpperCase()), Part.IgnoreCaseType.ALWAYS); + query = new CosmosQuery(criteria); + result = TestUtils.toList(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 testFindByIdWithPartitionNotExists() { + final PartitionPerson partitionPersonById = cosmosTemplate.findById(NOT_EXIST_ID, + PartitionPerson.class, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + + assertNull(partitionPersonById); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testFindByNonExistIdWithPartition() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, PROPERTY_ID, + Collections.singletonList(NOT_EXIST_ID), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + + final List result = TestUtils.toList(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_ZIP_CODE, + null, null); + final PartitionPerson partitionPerson = + cosmosTemplate.upsertAndReturnEntity(PartitionPerson.class.getSimpleName(), newPerson); + + final List result = TestUtils.toList(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.getZipCode(), TEST_PERSON.getHobbies(), TEST_PERSON.getShippingAddresses()); + final PartitionPerson partitionPerson = + cosmosTemplate.upsertAndReturnEntity(PartitionPerson.class.getSimpleName(), updated); + + assertEquals(partitionPerson, updated); + } + + @Test + public void testDeleteByIdPartition() { + // insert new document with same partition key + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())); + + final List inserted = TestUtils.toList(cosmosTemplate.findAll(PartitionPerson.class)); + assertThat(inserted.size()).isEqualTo(2); + + cosmosTemplate.deleteById(PartitionPerson.class.getSimpleName(), + TEST_PERSON.getId(), new PartitionKey(TEST_PERSON.getZipCode())); + + final List result = TestUtils.toList(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.getZipCode())); + + final long newCount = cosmosTemplate.count(containerName); + assertThat(newCount).isEqualTo(2); + } + + @Test + public void testCountForPartitionedCollectionByQuery() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON_2.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + + final long count = cosmosTemplate.count(query, containerName); + assertThat(count).isEqualTo(1); + } + + @Test + public void testCountIgnoreCaseForPartitionedCollectionByQuery() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())); + final Criteria criteriaIgnoreCase = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON_2.getFirstName().toUpperCase()), Part.IgnoreCaseType.ALWAYS); + final CosmosQuery queryIgnoreCase = new CosmosQuery(criteriaIgnoreCase); + + final long countIgnoreCase = cosmosTemplate.count(queryIgnoreCase, containerName); + assertThat(countIgnoreCase).isEqualTo(1); + } + + @Test + public void testNonExistFieldValue() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList("non-exist-first-name"), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + + final long count = cosmosTemplate.count(query, containerName); + assertThat(count).isEqualTo(0); + } + + @Test + public void testPartitionedFindAllPageableMultiPages() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())); + + 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); + PageTestUtils.validateNonLastPage(page1, PAGE_SIZE_1); + + final Page page2 = cosmosTemplate.findAll(page1.nextPageable(), + PartitionPerson.class, containerName); + assertThat(page2.getContent().size()).isEqualTo(1); + PageTestUtils.validateLastPage(page2, PAGE_SIZE_1); + } + + @Test + public void testPartitionedPaginationQuery() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME), Part.IgnoreCaseType.NEVER); + final PageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_2, null); + final CosmosQuery query = new CosmosQuery(criteria).with(pageRequest); + + final Page page = cosmosTemplate.paginationQuery(query, PartitionPerson.class, containerName); + assertThat(page.getContent().size()).isEqualTo(1); + PageTestUtils.validateLastPage(page, PAGE_SIZE_2); + } + + @Test + public void testPartitionedPaginationQueryWithOneOffset() { + cosmosTemplate.insert(TEST_PERSON_3, new PartitionKey(TEST_PERSON_3.getZipCode())); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME), Part.IgnoreCaseType.NEVER); + final PageRequest pageRequest = CosmosPageRequest.of(1, 0, PAGE_SIZE_2, + null, Sort.by(ASC, "id")); + final CosmosQuery query = new CosmosQuery(criteria).with(pageRequest).with(pageRequest.getSort()); + + final Page page = cosmosTemplate.paginationQuery(query, PartitionPerson.class, containerName); + assertThat(page.getContent().size()).isEqualTo(PAGE_SIZE_1); + assertThat(page.getContent().get(0).getId()).isEqualTo(ID_3); + PageTestUtils.validateLastPage(page, PAGE_SIZE_2); + } + + @Test + public void testPartionedPaginationQueryWithOneOffsetAndMultiplePages() { + cosmosTemplate.insert(TEST_PERSON_3, new PartitionKey(TEST_PERSON_3.getZipCode())); + PartitionPerson TEST_PERSON_4 = new PartitionPerson("id-4", FIRST_NAME, NEW_ZIP_CODE, + HOBBIES, ADDRESSES); + cosmosTemplate.insert(TEST_PERSON_4, new PartitionKey(TEST_PERSON_4.getZipCode())); + PartitionPerson TEST_PERSON_5 = new PartitionPerson("id-5", FIRST_NAME, NEW_ZIP_CODE, + HOBBIES, ADDRESSES); + cosmosTemplate.insert(TEST_PERSON_5, new PartitionKey(TEST_PERSON_5.getZipCode())); + PartitionPerson TEST_PERSON_6 = new PartitionPerson("id-6", FIRST_NAME, NEW_ZIP_CODE, + HOBBIES, ADDRESSES); + cosmosTemplate.insert(TEST_PERSON_6, new PartitionKey(TEST_PERSON_6.getZipCode())); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME), Part.IgnoreCaseType.NEVER); + final PageRequest pageRequest = CosmosPageRequest.of(1, 0, PAGE_SIZE_2, + null, Sort.by(ASC, "id")); + final CosmosQuery query = new CosmosQuery(criteria).with(pageRequest).with(pageRequest.getSort()); + + final Page page = cosmosTemplate.paginationQuery(query, PartitionPerson.class, containerName); + assertThat(page.getContent().size()).isEqualTo(PAGE_SIZE_2); + assertThat(page.getContent().get(0).getId()).isEqualTo(ID_3); + assertThat(page.getContent().get(1).getId()).isEqualTo("id-4"); + PageTestUtils.validateNonLastPage(page, page.getContent().size()); + + final CosmosQuery queryPage2 = new CosmosQuery(criteria).with(page.nextPageable()) + .with(page.nextPageable().getSort()); + + final Page page2 = cosmosTemplate.paginationQuery(queryPage2, PartitionPerson.class, containerName); + assertThat(page2.getContent().size()).isEqualTo(PAGE_SIZE_2); + assertThat(page2.getContent().get(0).getId()).isEqualTo("id-5"); + assertThat(page2.getContent().get(1).getId()).isEqualTo("id-6"); + PageTestUtils.validateNonLastPage(page, page.getContent().size()); + PageTestUtils.validateLastPage(page2, page2.getContent().size()); + } + + @Test + public void testPartitionedPaginationQueryWithZeroOffset() { + cosmosTemplate.insert(TEST_PERSON_3, new PartitionKey(TEST_PERSON_3.getZipCode())); + + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME), Part.IgnoreCaseType.NEVER); + final PageRequest pageRequest = CosmosPageRequest.of(0, 0, PAGE_SIZE_2, + null, Sort.by(ASC, "id")); + final CosmosQuery query = new CosmosQuery(criteria).with(pageRequest).with(pageRequest.getSort()); + + final Page page = cosmosTemplate.paginationQuery(query, PartitionPerson.class, containerName); + assertThat(page.getContent().size()).isEqualTo(2); + assertThat(page.getContent().get(0).getId()).isEqualTo(ID_1); + assertThat(page.getContent().get(1).getId()).isEqualTo(ID_3); + PageTestUtils.validateLastPage(page, page.getContent().size()); + } + + @Test + public void testPartitionedPaginationQueryIgnoreCase() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())); + final Criteria criteriaIgnoreCase = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(FIRST_NAME.toUpperCase()), Part.IgnoreCaseType.ALWAYS); + final PageRequest pageRequest = new CosmosPageRequest(0, PAGE_SIZE_2, null); + final CosmosQuery queryIgnoreCase = new CosmosQuery(criteriaIgnoreCase).with(pageRequest); + + final Page pageIgnoreCase = cosmosTemplate + .paginationQuery(queryIgnoreCase, PartitionPerson.class, containerName); + assertThat(pageIgnoreCase.getContent().size()).isEqualTo(1); + PageTestUtils.validateLastPage(pageIgnoreCase, PAGE_SIZE_2); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantContainerCosmosFactory.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantContainerCosmosFactory.java new file mode 100644 index 000000000000..03d5ee1fe659 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantContainerCosmosFactory.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.spring.data.cosmos.CosmosFactory; + +/** + * Example for extending CosmosFactory for Mutli-Tenancy at the container level + */ +public class MultiTenantContainerCosmosFactory extends CosmosFactory { + + public String manuallySetDatabaseName; + + public String manuallySetContainerName; + + /** + * Validate config and initialization + * + * @param cosmosAsyncClient cosmosAsyncClient + * @param databaseName databaseName + */ + public MultiTenantContainerCosmosFactory(CosmosAsyncClient cosmosAsyncClient, String databaseName) { + super(cosmosAsyncClient, databaseName); + + this.manuallySetDatabaseName = databaseName; + } + + @Override + public String overrideContainerName() { + return this.manuallySetContainerName; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantContainerCosmosFactoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantContainerCosmosFactoryIT.java new file mode 100644 index 000000000000..a301a7d65e97 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantContainerCosmosFactoryIT.java @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosAsyncDatabase; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.CosmosException; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.repository.MultiTenantTestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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 java.util.ArrayList; +import java.util.List; + +import static com.azure.spring.data.cosmos.common.TestConstants.ADDRESSES; +import static com.azure.spring.data.cosmos.common.TestConstants.AGE; +import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_1; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_2; +import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = MultiTenantTestRepositoryConfig.class) +public class MultiTenantContainerCosmosFactoryIT { + + private final String testDB1 = "Database1"; + + private final String testContainer1= "Container1"; + private final String testContainer2 = "Container2"; + + private final Person TEST_PERSON_1 = new Person(ID_1, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + private final Person TEST_PERSON_2 = new Person(ID_2, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosConfig cosmosConfig; + @Autowired + private CosmosClientBuilder cosmosClientBuilder; + + private MultiTenantContainerCosmosFactory cosmosFactory; + private CosmosTemplate cosmosTemplate; + private CosmosAsyncClient client; + private CosmosEntityInformation personInfo; + + @Before + public void setUp() throws ClassNotFoundException { + /// Setup + client = CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + cosmosFactory = new MultiTenantContainerCosmosFactory(client, testDB1); + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + + try { + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + } catch (Exception e) { + Assert.fail(); + } + + final MappingCosmosConverter cosmosConverter = new MappingCosmosConverter(mappingContext, null); + cosmosTemplate = new CosmosTemplate(cosmosFactory, cosmosConfig, cosmosConverter, null); + personInfo = new CosmosEntityInformation<>(Person.class); + } + + @Test + public void testGetContainerFunctionality() { + // Create testContainer1 and add TEST_PERSON_1 to it + cosmosFactory.manuallySetContainerName = testContainer1; + cosmosTemplate.createContainerIfNotExists(personInfo); + cosmosTemplate.deleteAll(personInfo.getContainerName(), Person.class); + assertThat(cosmosFactory.overrideContainerName()).isEqualTo(testContainer1); + cosmosTemplate.insert(TEST_PERSON_1, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_1))); + + // Create testContainer1 and add TEST_PERSON_2 to it + cosmosFactory.manuallySetContainerName = testContainer2; + cosmosTemplate.createContainerIfNotExists(personInfo); + cosmosTemplate.deleteAll(personInfo.getContainerName(), Person.class); + assertThat(cosmosFactory.overrideContainerName()).isEqualTo(testContainer2); + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + + // Check that testContainer2 has the correct contents + List expectedResultsContainer2 = new ArrayList<>(); + expectedResultsContainer2.add(TEST_PERSON_2); + Iterable iterableDB2 = cosmosTemplate.findAll(personInfo.getContainerName(), Person.class); + List resultDB2 = new ArrayList<>(); + iterableDB2.forEach(resultDB2::add); + Assert.assertEquals(expectedResultsContainer2, resultDB2); + + // Check that testContainer1 has the correct contents + cosmosFactory.manuallySetContainerName = testContainer1; + List expectedResultsContainer1 = new ArrayList<>(); + expectedResultsContainer1.add(TEST_PERSON_1); + Iterable iterableDB1 = cosmosTemplate.findAll(personInfo.getContainerName(), Person.class); + List resultDB1 = new ArrayList<>(); + iterableDB1.forEach(resultDB1::add); + Assert.assertEquals(expectedResultsContainer1, resultDB1); + + //Cleanup + deleteDatabaseIfExists(testDB1); + } + + private void deleteDatabaseIfExists(String dbName) { + CosmosAsyncDatabase database = client.getDatabase(dbName); + try { + database.delete().block(); + } catch (CosmosException ex) { + assertEquals(ex.getStatusCode(), 404); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantDBCosmosFactory.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantDBCosmosFactory.java new file mode 100644 index 000000000000..d7212896d6e7 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantDBCosmosFactory.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.spring.data.cosmos.CosmosFactory; + +/** + * Example for extending CosmosFactory for Mutli-Tenancy at the database level + */ +public class MultiTenantDBCosmosFactory extends CosmosFactory { + + public String manuallySetDatabaseName; + + /** + * Validate config and initialization + * + * @param cosmosAsyncClient cosmosAsyncClient + * @param databaseName databaseName + */ + public MultiTenantDBCosmosFactory(CosmosAsyncClient cosmosAsyncClient, String databaseName) { + super(cosmosAsyncClient, databaseName); + + this.manuallySetDatabaseName = databaseName; + } + + @Override + public String getDatabaseName() { + return this.manuallySetDatabaseName; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantDBCosmosFactoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantDBCosmosFactoryIT.java new file mode 100644 index 000000000000..dc16252d651a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/MultiTenantDBCosmosFactoryIT.java @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosAsyncDatabase; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.CosmosException; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.repository.MultiTenantTestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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 java.util.ArrayList; +import java.util.List; + +import static com.azure.spring.data.cosmos.common.TestConstants.ADDRESSES; +import static com.azure.spring.data.cosmos.common.TestConstants.AGE; +import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_1; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_2; +import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = MultiTenantTestRepositoryConfig.class) +public class MultiTenantDBCosmosFactoryIT { + + private final String testDB1 = "Database1"; + private final String testDB2 = "Database2"; + + private final Person TEST_PERSON_1 = new Person(ID_1, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + private final Person TEST_PERSON_2 = new Person(ID_2, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosConfig cosmosConfig; + @Autowired + private CosmosClientBuilder cosmosClientBuilder; + + private MultiTenantDBCosmosFactory cosmosFactory; + private CosmosTemplate cosmosTemplate; + private CosmosAsyncClient client; + private CosmosEntityInformation personInfo; + + @Before + public void setUp() throws ClassNotFoundException { + /// Setup + client = CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + cosmosFactory = new MultiTenantDBCosmosFactory(client, testDB1); + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + + try { + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + } catch (Exception e) { + Assert.fail(); + } + + final MappingCosmosConverter cosmosConverter = new MappingCosmosConverter(mappingContext, null); + cosmosTemplate = new CosmosTemplate(cosmosFactory, cosmosConfig, cosmosConverter, null); + personInfo = new CosmosEntityInformation<>(Person.class); + } + + @Test + public void testGetDatabaseFunctionality() { + // Create DB1 and add TEST_PERSON_1 to it + cosmosTemplate.createContainerIfNotExists(personInfo); + cosmosTemplate.deleteAll(personInfo.getContainerName(), Person.class); + assertThat(cosmosFactory.getDatabaseName()).isEqualTo(testDB1); + cosmosTemplate.insert(TEST_PERSON_1, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_1))); + + // Create DB2 and add TEST_PERSON_2 to it + cosmosFactory.manuallySetDatabaseName = testDB2; + cosmosTemplate.createContainerIfNotExists(personInfo); + cosmosTemplate.deleteAll(personInfo.getContainerName(), Person.class); + assertThat(cosmosFactory.getDatabaseName()).isEqualTo(testDB2); + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))); + + // Check that DB2 has the correct contents + List expectedResultsDB2 = new ArrayList<>(); + expectedResultsDB2.add(TEST_PERSON_2); + Iterable iterableDB2 = cosmosTemplate.findAll(personInfo.getContainerName(), Person.class); + List resultDB2 = new ArrayList<>(); + iterableDB2.forEach(resultDB2::add); + Assert.assertEquals(expectedResultsDB2, resultDB2); + + // Check that DB1 has the correct contents + cosmosFactory.manuallySetDatabaseName = testDB1; + List expectedResultsDB1 = new ArrayList<>(); + expectedResultsDB1.add(TEST_PERSON_1); + Iterable iterableDB1 = cosmosTemplate.findAll(personInfo.getContainerName(), Person.class); + List resultDB1 = new ArrayList<>(); + iterableDB1.forEach(resultDB1::add); + Assert.assertEquals(expectedResultsDB1, resultDB1); + + //Cleanup + deleteDatabaseIfExists(testDB1); + deleteDatabaseIfExists(testDB2); + } + + private void deleteDatabaseIfExists(String dbName) { + CosmosAsyncDatabase database = client.getDatabase(dbName); + try { + database.delete().block(); + } catch (CosmosException ex) { + assertEquals(ex.getStatusCode(), 404); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplateIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplateIT.java new file mode 100644 index 000000000000..0b8982ddab23 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplateIT.java @@ -0,0 +1,742 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core; + +import com.azure.core.credential.AzureKeyCredential; +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosAsyncDatabase; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.CosmosException; +import com.azure.cosmos.implementation.ConflictException; +import com.azure.cosmos.implementation.PreconditionFailedException; +import com.azure.cosmos.models.CosmosContainerResponse; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.cosmos.models.ThroughputResponse; +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.PropertyLoader; +import com.azure.spring.data.cosmos.common.ResponseDiagnosticsTestUtils; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.generator.FindQuerySpecGenerator; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.domain.AuditableEntity; +import com.azure.spring.data.cosmos.domain.AutoScaleSample; +import com.azure.spring.data.cosmos.domain.GenIdEntity; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.AuditableRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.domain.EntityScanner; +import org.springframework.context.ApplicationContext; +import org.springframework.data.annotation.Persistent; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.util.ReflectionTestUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +import static com.azure.spring.data.cosmos.common.TestConstants.ADDRESSES; +import static com.azure.spring.data.cosmos.common.TestConstants.AGE; +import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_PASSPORT_IDS_BY_COUNTRY; +import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_1; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_AGE_INCREMENT; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.PATCH_HOBBY1; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +@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, AGE, PASSPORT_IDS_BY_COUNTRY); + + private static final Person TEST_PERSON_2 = new Person(TestConstants.ID_2, TestConstants.NEW_FIRST_NAME, + TestConstants.NEW_LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + private static final Person TEST_PERSON_3 = new Person(TestConstants.ID_3, TestConstants.NEW_FIRST_NAME, + TestConstants.NEW_LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + private static final Person TEST_PERSON_4 = new Person(TestConstants.ID_4, TestConstants.NEW_FIRST_NAME, + TestConstants.NEW_LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + private static final String PRECONDITION_IS_NOT_MET = "is not met"; + private static final String WRONG_ETAG = "WRONG_ETAG"; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final JsonNode NEW_PASSPORT_IDS_BY_COUNTRY_JSON = OBJECT_MAPPER.convertValue(NEW_PASSPORT_IDS_BY_COUNTRY, JsonNode.class); + + private static final CosmosPatchOperations operations = CosmosPatchOperations + .create() + .replace("/age", PATCH_AGE_1); + + CosmosPatchOperations multiPatchOperations = CosmosPatchOperations + .create() + .set("/firstName", PATCH_FIRST_NAME) + .replace("/passportIdsByCountry", NEW_PASSPORT_IDS_BY_COUNTRY_JSON) + .add("/hobbies/2", PATCH_HOBBY1) + .remove("/shippingAddresses/1") + .increment("/age", PATCH_AGE_INCREMENT); + + private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions(); + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); + + @Value("${cosmos.secondaryKey}") + private String cosmosDbSecondaryKey; + + @Value("${cosmos.key}") + private String cosmosDbKey; + + private static CosmosAsyncClient client; + private static ReactiveCosmosTemplate cosmosTemplate; + private static String containerName; + private static CosmosEntityInformation personInfo; + private static AzureKeyCredential azureKeyCredential; + + private Person insertedPerson; + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosConfig cosmosConfig; + @Autowired + private CosmosClientBuilder cosmosClientBuilder; + @Autowired + private ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils; + @Autowired + private AuditableRepository auditableRepository; + + @Before + public void setUp() throws ClassNotFoundException { + if (cosmosTemplate == null) { + azureKeyCredential = new AzureKeyCredential(cosmosDbKey); + cosmosClientBuilder.credential(azureKeyCredential); + client = CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + personInfo = new CosmosEntityInformation<>(Person.class); + containerName = personInfo.getContainerName(); + cosmosTemplate = createReactiveCosmosTemplate(cosmosConfig, TestConstants.DB_NAME); + } + + collectionManager.ensureContainersCreatedAndEmpty(cosmosTemplate, Person.class, GenIdEntity.class, AuditableEntity.class); + + insertedPerson = cosmosTemplate.insert(TEST_PERSON, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))).block(); + } + + private ReactiveCosmosTemplate createReactiveCosmosTemplate(CosmosConfig config, String dbName) throws ClassNotFoundException { + final CosmosFactory cosmosFactory = new CosmosFactory(client, dbName); + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + final MappingCosmosConverter cosmosConverter = new MappingCosmosConverter(mappingContext, null); + return new ReactiveCosmosTemplate(cosmosFactory, config, cosmosConverter); + } + + @After + public void cleanup() { + // Reset master key + azureKeyCredential.update(cosmosDbKey); + } + + @Test + public void testInsertDuplicateId() { + final Mono insertMono = cosmosTemplate.insert(TEST_PERSON, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + StepVerifier.create(insertMono) + .expectErrorMatches(ex -> ex instanceof CosmosAccessException && + ((CosmosAccessException) ex).getCosmosException() instanceof ConflictException) + .verify(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @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.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindByIDBySecondaryKey() { + azureKeyCredential.update(cosmosDbSecondaryKey); + final Mono findById = cosmosTemplate.findById(Person.class.getSimpleName(), + TEST_PERSON.getId(), + Person.class); + StepVerifier.create(findById).consumeNextWith(actual -> { + Assert.assertEquals(actual.getFirstName(), TEST_PERSON.getFirstName()); + Assert.assertEquals(actual.getLastName(), TEST_PERSON.getLastName()); + }).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.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.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.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.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.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(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testInsertBySecondaryKey() { + azureKeyCredential.update(cosmosDbSecondaryKey); + StepVerifier.create(cosmosTemplate.insert(TEST_PERSON_3, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_3)))) + .expectNext(TEST_PERSON_3).verifyComplete(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testInsertShouldFailIfColumnNotAnnotatedWithAutoGenerate() { + final Person person = new Person(null, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + Mono entityMono = cosmosTemplate.insert(Person.class.getSimpleName(), + person, new PartitionKey(person.getLastName())); + StepVerifier.create(entityMono).verifyError(CosmosAccessException.class); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testInsertShouldGenerateIdIfColumnAnnotatedWithAutoGenerate() { + final GenIdEntity entity = new GenIdEntity(null, "foo"); + final Mono insertedEntityMono = cosmosTemplate.insert(GenIdEntity.class.getSimpleName(), + entity, null); + GenIdEntity insertedEntity = insertedEntityMono.block(); + assertThat(insertedEntity).isNotNull(); + assertThat(insertedEntity.getId()).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); + StepVerifier.create(upsert).expectNextCount(1).verifyComplete(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testPatch() { + final Mono patch = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Mono patchedPerson = cosmosTemplate.findById(containerName, insertedPerson.getId(), Person.class); + StepVerifier.create(patchedPerson).expectNextMatches(person -> person.getAge() == PATCH_AGE_1).verifyComplete(); + } + + @Test + public void testPatchMultiOperations() { + final Mono patch = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, multiPatchOperations); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Person patchedPerson = cosmosTemplate.findById(containerName, insertedPerson.getId(), Person.class).block(); + assertEquals(patchedPerson.getAge().intValue(), (AGE + PATCH_AGE_INCREMENT)); + assertEquals(patchedPerson.getHobbies(),PATCH_HOBBIES); + assertEquals(patchedPerson.getFirstName(), PATCH_FIRST_NAME); + assertEquals(patchedPerson.getShippingAddresses().size(), 1); + assertEquals(patchedPerson.getPassportIdsByCountry(), NEW_PASSPORT_IDS_BY_COUNTRY); + } + + @Test + public void testPatchPreConditionSuccess() { + options.setFilterPredicate("FROM person p WHERE p.lastName = '"+LAST_NAME+"'"); + Mono patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options); + StepVerifier.create(patchedPerson).expectNextMatches(person -> person.getAge() == PATCH_AGE_1).verifyComplete(); + } + + @Test + public void testPatchPreConditionFail() { + options.setFilterPredicate("FROM person p WHERE p.lastName = 'dummy'"); + Mono person = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options); + StepVerifier.create(person).expectErrorMatches(ex -> ex instanceof CosmosAccessException && + ((CosmosAccessException) ex).getCosmosException() instanceof PreconditionFailedException).verify(); + } + + @Test + public void testOptimisticLockWhenUpdatingWithWrongEtag() { + final Person updated = new Person(TEST_PERSON.getId(), TestConstants.UPDATED_FIRST_NAME, + TEST_PERSON.getLastName(), TEST_PERSON.getHobbies(), TEST_PERSON.getShippingAddresses(), + AGE, PASSPORT_IDS_BY_COUNTRY); + updated.set_etag(WRONG_ETAG); + + try { + cosmosTemplate.upsert(updated).block(); + } catch (CosmosAccessException cosmosAccessException) { + assertThat(cosmosAccessException.getCosmosException()).isNotNull(); + final Throwable cosmosClientException = cosmosAccessException.getCosmosException(); + assertThat(cosmosClientException).isInstanceOf(CosmosException.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() { + azureKeyCredential.update(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); + StepVerifier.create(upsert).expectNextCount(1).verifyComplete(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).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); + StepVerifier.create(upsert).expectNextCount(1).verifyComplete(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testDeleteById() { + cosmosTemplate.insert(TEST_PERSON_4, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_4))).block(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + + Flux flux = cosmosTemplate.findAll(Person.class.getSimpleName(), Person.class); + StepVerifier.create(flux).expectNextCount(2).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + + final Mono voidMono = cosmosTemplate.deleteById(Person.class.getSimpleName(), + TEST_PERSON_4.getId(), + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_4))); + StepVerifier.create(voidMono).verifyComplete(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + + flux = cosmosTemplate.findAll(Person.class.getSimpleName(), Person.class); + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + } + + @Test + public void testDeleteByEntity() { + Person insertedPerson = cosmosTemplate.insert(TEST_PERSON_4, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_4))).block(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + + Flux flux = cosmosTemplate.findAll(Person.class.getSimpleName(), Person.class); + StepVerifier.create(flux).expectNextCount(2).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + + final Mono voidMono = cosmosTemplate.deleteEntity(Person.class.getSimpleName(), insertedPerson); + StepVerifier.create(voidMono).verifyComplete(); + + + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + + flux = cosmosTemplate.findAll(Person.class.getSimpleName(), Person.class); + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + } + + @Test + public void testDeleteByIdBySecondaryKey() { + azureKeyCredential.update(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", + Collections.singletonList(TEST_PERSON.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + final Flux personFlux = cosmosTemplate.find(query, Person.class, + Person.class.getSimpleName()); + StepVerifier.create(personFlux).expectNextCount(1).verifyComplete(); + + // add ignore testing + final Criteria criteriaIgnoreCase = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName().toUpperCase()), Part.IgnoreCaseType.ALWAYS); + final CosmosQuery queryIgnoreCase = new CosmosQuery(criteriaIgnoreCase); + final Flux personFluxIgnoreCase = cosmosTemplate.find(queryIgnoreCase, Person.class, + Person.class.getSimpleName()); + StepVerifier.create(personFluxIgnoreCase).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testExists() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + final Mono exists = cosmosTemplate.exists(query, Person.class, containerName); + StepVerifier.create(exists).expectNext(true).verifyComplete(); + + // add ignore testing + final Criteria criteriaIgnoreCase = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON.getFirstName().toUpperCase()), Part.IgnoreCaseType.ALWAYS); + final CosmosQuery queryIgnoreCase = new CosmosQuery(criteriaIgnoreCase); + final Mono existsIgnoreCase = cosmosTemplate.exists(queryIgnoreCase, Person.class, containerName); + StepVerifier.create(existsIgnoreCase).expectNext(true).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testCount() { + final Mono count = cosmosTemplate.count(containerName); + StepVerifier.create(count).expectNext((long) 1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testCountBySecondaryKey() { + azureKeyCredential.update(cosmosDbSecondaryKey); + final Mono count = cosmosTemplate.count(containerName); + StepVerifier.create(count).expectNext((long) 1).verifyComplete(); + } + + @Test + public void testInvalidSecondaryKey() { + azureKeyCredential.update("Invalid secondary key"); + final Mono findById = cosmosTemplate.findById(Person.class.getSimpleName(), + TEST_PERSON.getId(), + Person.class); + StepVerifier.create(findById) + .expectError(CosmosAccessException.class) + .verify(); + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testRunQueryWithSimpleReturnType() { + Criteria ageBetween = Criteria.getInstance(CriteriaType.BETWEEN, "age", Arrays.asList(AGE - 1, AGE + 1), + Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(ageBetween)); + final Flux flux = cosmosTemplate.runQuery(sqlQuerySpec, Person.class, Person.class); + + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + } + + @Test + public void testRunQueryWithReturnTypeContainingLocalDateTime() { + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + + auditableRepository.save(entity); + + Criteria equals = Criteria.getInstance(CriteriaType.IS_EQUAL, "id", Collections.singletonList(entity.getId()), Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(equals)); + final Flux flux = cosmosTemplate.runQuery(sqlQuerySpec, AuditableEntity.class, AuditableEntity.class); + + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + } + + @Test + public void testFindWithEqualCriteriaContainingNestedProperty() { + String postalCode = ADDRESSES.get(0).getPostalCode(); + String subjectWithNestedProperty = "shippingAddresses[0]['postalCode']"; + Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, subjectWithNestedProperty, + Collections.singletonList(postalCode), Part.IgnoreCaseType.NEVER); + + final Flux people = cosmosTemplate.find(new CosmosQuery(criteria), Person.class, containerName); + + StepVerifier.create(people).expectNextCount(1).verifyComplete(); + } + + @Test + public void testRunQueryWithEqualCriteriaContainingSpecialChars() { + String ivoryCoastPassportId = PASSPORT_IDS_BY_COUNTRY.get("Côte d'Ivoire"); + String subjectWithSpecialChars = "passportIdsByCountry[\"Côte d'Ivoire\"]"; + Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, subjectWithSpecialChars, + Collections.singletonList(ivoryCoastPassportId), Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(criteria)); + + final Flux people = cosmosTemplate.runQuery(sqlQuerySpec, Person.class, Person.class); + + StepVerifier.create(people).expectNextCount(1).verifyComplete(); + } + + @Test + public void createWithAutoscale() { + final CosmosEntityInformation autoScaleSampleInfo = + new CosmosEntityInformation<>(AutoScaleSample.class); + CosmosContainerResponse containerResponse = cosmosTemplate + .createContainerIfNotExists(autoScaleSampleInfo) + .block(); + assertNotNull(containerResponse); + ThroughputResponse throughput = client.getDatabase(TestConstants.DB_NAME) + .getContainer(autoScaleSampleInfo.getContainerName()) + .readThroughput() + .block(); + assertNotNull(throughput); + assertEquals(Integer.parseInt(TestConstants.AUTOSCALE_MAX_THROUGHPUT), + throughput.getProperties().getAutoscaleMaxThroughput()); + } + + @Test + public void createDatabaseWithThroughput() throws ClassNotFoundException { + final String configuredThroughputDbName = TestConstants.DB_NAME + "-other"; + deleteDatabaseIfExists(configuredThroughputDbName); + + Integer expectedRequestUnits = 700; + final CosmosConfig config = CosmosConfig.builder() + .enableDatabaseThroughput(false, expectedRequestUnits) + .build(); + final ReactiveCosmosTemplate configuredThroughputCosmosTemplate = createReactiveCosmosTemplate(config, configuredThroughputDbName); + + final CosmosEntityInformation personInfo = + new CosmosEntityInformation<>(Person.class); + configuredThroughputCosmosTemplate.createContainerIfNotExists(personInfo).block(); + + final CosmosAsyncDatabase database = client.getDatabase(configuredThroughputDbName); + final ThroughputResponse response = database.readThroughput().block(); + assertEquals(expectedRequestUnits, response.getProperties().getManualThroughput()); + } + + @Test + public void queryWithMaxDegreeOfParallelism() throws ClassNotFoundException { + final CosmosConfig config = CosmosConfig.builder() + .maxDegreeOfParallelism(20) + .build(); + final ReactiveCosmosTemplate maxDegreeOfParallelismCosmosTemplate = createReactiveCosmosTemplate(config, TestConstants.DB_NAME); + + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + + auditableRepository.save(entity); + + Criteria equals = Criteria.getInstance(CriteriaType.IS_EQUAL, "id", Collections.singletonList(entity.getId()), Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(equals)); + final Flux flux = maxDegreeOfParallelismCosmosTemplate.runQuery(sqlQuerySpec, AuditableEntity.class, AuditableEntity.class); + + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + assertEquals((int) ReflectionTestUtils.getField(maxDegreeOfParallelismCosmosTemplate, "maxDegreeOfParallelism"), 20); + } + + @Test + public void queryWithMaxBufferedItemCount() throws ClassNotFoundException { + final CosmosConfig config = CosmosConfig.builder() + .maxBufferedItemCount(500) + .build(); + final ReactiveCosmosTemplate maxBufferedItemCountCosmosTemplate = createReactiveCosmosTemplate(config, TestConstants.DB_NAME); + + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + + auditableRepository.save(entity); + + Criteria equals = Criteria.getInstance(CriteriaType.IS_EQUAL, "id", Collections.singletonList(entity.getId()), Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(equals)); + final Flux flux = maxBufferedItemCountCosmosTemplate.runQuery(sqlQuerySpec, AuditableEntity.class, AuditableEntity.class); + + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + assertEquals((int) ReflectionTestUtils.getField(maxBufferedItemCountCosmosTemplate, "maxBufferedItemCount"), 500); + } + + @Test + public void queryWithResponseContinuationTokenLimitInKb() throws ClassNotFoundException { + final CosmosConfig config = CosmosConfig.builder() + .responseContinuationTokenLimitInKb(2000) + .build(); + final ReactiveCosmosTemplate responseContinuationTokenLimitInKbCosmosTemplate = + createReactiveCosmosTemplate(config, TestConstants.DB_NAME); + + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + + auditableRepository.save(entity); + + Criteria equals = Criteria.getInstance(CriteriaType.IS_EQUAL, "id", Collections.singletonList(entity.getId()), Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(equals)); + final Flux flux = responseContinuationTokenLimitInKbCosmosTemplate.runQuery(sqlQuerySpec, AuditableEntity.class, AuditableEntity.class); + + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + assertEquals((int) ReflectionTestUtils.getField(responseContinuationTokenLimitInKbCosmosTemplate, + "responseContinuationTokenLimitInKb"), 2000); + } + + @Test + public void queryWithQueryMerticsEnabled() throws ClassNotFoundException { + final CosmosConfig config = CosmosConfig.builder() + .enableQueryMetrics(true) + .build(); + final ReactiveCosmosTemplate queryMetricsEnabledCosmosTemplate = createReactiveCosmosTemplate(config, TestConstants.DB_NAME); + + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + + auditableRepository.save(entity); + + Criteria equals = Criteria.getInstance(CriteriaType.IS_EQUAL, "id", Collections.singletonList(entity.getId()), Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(equals)); + final Flux flux = queryMetricsEnabledCosmosTemplate.runQuery(sqlQuerySpec, AuditableEntity.class, AuditableEntity.class); + + StepVerifier.create(flux).expectNextCount(1).verifyComplete(); + assertEquals((boolean) ReflectionTestUtils.getField(queryMetricsEnabledCosmosTemplate, "queryMetricsEnabled"), true); + } + + @Test + public void userAgentSpringDataCosmosSuffix() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + // getUserAgentSuffix method from CosmosClientBuilder + Method getUserAgentSuffix = CosmosClientBuilder.class.getDeclaredMethod("getUserAgentSuffix"); + getUserAgentSuffix.setAccessible(true); + String userAgentSuffix = (String) getUserAgentSuffix.invoke(cosmosClientBuilder); + assertThat(userAgentSuffix).contains(Constants.USER_AGENT_SUFFIX); + assertThat(userAgentSuffix).contains(PropertyLoader.getProjectVersion()); + } + + private void deleteDatabaseIfExists(String dbName) { + CosmosAsyncDatabase database = client.getDatabase(dbName); + try { + database.delete().block(); + } catch (CosmosException ex) { + assertEquals(ex.getStatusCode(), 404); + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplatePartitionIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplatePartitionIT.java new file mode 100644 index 000000000000..a28a47207559 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplatePartitionIT.java @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosBridgeInternal; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.implementation.AsyncDocumentClient; +import com.azure.cosmos.implementation.query.PartitionedQueryExecutionInfo; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.ResponseDiagnosticsTestUtils; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.generator.FindQuerySpecGenerator; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.domain.PartitionPerson; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.assertj.core.api.Assertions; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.repository.query.parser.Part; +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.Collections; +import java.util.Map; +import java.util.UUID; + +import static com.azure.spring.data.cosmos.common.TestConstants.NOT_EXIST_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveCosmosTemplatePartitionIT { + private static final PartitionPerson TEST_PERSON = new PartitionPerson(TestConstants.ID_1, + TestConstants.FIRST_NAME, TestConstants.ZIP_CODE, + TestConstants.HOBBIES, TestConstants.ADDRESSES); + + private static final PartitionPerson TEST_PERSON_2 = new PartitionPerson(TestConstants.ID_2, + TestConstants.NEW_FIRST_NAME, + TestConstants.NEW_ZIP_CODE, TestConstants.HOBBIES, TestConstants.ADDRESSES); + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); + + private static CosmosFactory cosmosFactory; + private static ReactiveCosmosTemplate cosmosTemplate; + private static String containerName; + private static CosmosEntityInformation personInfo; + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosConfig cosmosConfig; + @Autowired + private CosmosClientBuilder cosmosClientBuilder; + @Autowired + private ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils; + + @Before + public void setUp() throws ClassNotFoundException { + if (cosmosTemplate == null) { + // Query plan caching is enabled by default + CosmosAsyncClient client = CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + cosmosFactory = new CosmosFactory(client, TestConstants.DB_NAME); + + 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(cosmosFactory, cosmosConfig, dbConverter); + } + collectionManager.ensureContainersCreatedAndEmpty(cosmosTemplate, PartitionPerson.class); + cosmosTemplate.insert(TEST_PERSON).block(); + } + + @Test + public void testFindWithPartition() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, TestConstants.PROPERTY_ZIP_CODE, + Collections.singletonList(TestConstants.ZIP_CODE), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + final Flux partitionPersonFlux = cosmosTemplate.find(query, + PartitionPerson.class, + PartitionPerson.class.getSimpleName()); + StepVerifier.create(partitionPersonFlux).consumeNextWith(actual -> { + Assert.assertEquals(actual.getFirstName(), TEST_PERSON.getFirstName()); + Assert.assertEquals(actual.getZipCode(), TEST_PERSON.getZipCode()); + }).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindWithPartitionWithQueryPlanCachingEnabled() { + Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, TestConstants.PROPERTY_ZIP_CODE, + Collections.singletonList(TestConstants.ZIP_CODE), Part.IgnoreCaseType.NEVER); + CosmosQuery query = new CosmosQuery(criteria); + SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + Flux partitionPersonFlux = cosmosTemplate.find(query, + PartitionPerson.class, + PartitionPerson.class.getSimpleName()); + StepVerifier.create(partitionPersonFlux).consumeNextWith(actual -> { + Assert.assertEquals(actual.getFirstName(), TEST_PERSON.getFirstName()); + Assert.assertEquals(actual.getZipCode(), TEST_PERSON.getZipCode()); + }).verifyComplete(); + + CosmosAsyncClient cosmosAsyncClient = cosmosFactory.getCosmosAsyncClient(); + AsyncDocumentClient asyncDocumentClient = CosmosBridgeInternal.getAsyncDocumentClient(cosmosAsyncClient); + Map initialCache = asyncDocumentClient.getQueryPlanCache(); + assertThat(initialCache.containsKey(sqlQuerySpec.getQueryText())).isTrue(); + int initialSize = initialCache.size(); + + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())).block(); + + // Fire the same query with different partition key value to make sure query plan caching is enabled + criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, TestConstants.PROPERTY_ZIP_CODE, + Collections.singletonList(TestConstants.NEW_ZIP_CODE), Part.IgnoreCaseType.NEVER); + query = new CosmosQuery(criteria); + sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + partitionPersonFlux = cosmosTemplate.find(query, + PartitionPerson.class, + PartitionPerson.class.getSimpleName()); + StepVerifier.create(partitionPersonFlux).consumeNextWith(actual -> { + Assert.assertEquals(actual.getFirstName(), TEST_PERSON_2.getFirstName()); + Assert.assertEquals(actual.getZipCode(), TEST_PERSON_2.getZipCode()); + }).verifyComplete(); + + Map postQueryCallCache = asyncDocumentClient.getQueryPlanCache(); + assertThat(postQueryCallCache.containsKey(sqlQuerySpec.getQueryText())).isTrue(); + assertThat(postQueryCallCache.size()).isEqualTo(initialSize); + } + + @Test + public void testFindIgnoreCaseWithPartition() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, TestConstants.PROPERTY_ZIP_CODE, + Collections.singletonList(TestConstants.ZIP_CODE), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + final Flux partitionPersonFlux = cosmosTemplate.find(query, + PartitionPerson.class, + PartitionPerson.class.getSimpleName()); + StepVerifier.create(partitionPersonFlux).consumeNextWith(actual -> { + Assert.assertEquals(actual.getFirstName(), TEST_PERSON.getFirstName()); + Assert.assertEquals(actual.getZipCode(), TEST_PERSON.getZipCode()); + }).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + + @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.assertEquals(actual.getFirstName(), TEST_PERSON.getFirstName()); + Assert.assertEquals(actual.getZipCode(), TEST_PERSON.getZipCode()); + }).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testFindByIdWithPartitionNotExists() { + final Mono partitionPersonMono = cosmosTemplate.findById(NOT_EXIST_ID, + PartitionPerson.class, + new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON))); + StepVerifier.create(partitionPersonMono).expectNextCount(0).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testUpsertNewDocumentPartition() { + final String firstName = TestConstants.NEW_FIRST_NAME + "_" + UUID.randomUUID().toString(); + final PartitionPerson newPerson = new PartitionPerson(UUID.randomUUID().toString(), + firstName, TestConstants.NEW_ZIP_CODE, + null, null); + final Mono upsert = cosmosTemplate.upsert(newPerson); + StepVerifier.create(upsert).expectNextCount(1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + } + + @Test + public void testUpdateWithPartition() { + final PartitionPerson updated = new PartitionPerson(TEST_PERSON.getId(), TestConstants.UPDATED_FIRST_NAME, + TEST_PERSON.getZipCode(), TEST_PERSON.getHobbies(), + TEST_PERSON.getShippingAddresses()); + cosmosTemplate.upsert(updated).block(); + + final PartitionPerson person = cosmosTemplate + .findAll(PartitionPerson.class.getSimpleName(), PartitionPerson.class) + .toStream() + .filter(p -> TEST_PERSON.getId().equals(p.getId())) + .findFirst().get(); + assertEquals(person, updated); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testDeleteByIdPartition() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())).block(); + StepVerifier.create(cosmosTemplate.findAll(PartitionPerson.class)).expectNextCount(2).verifyComplete(); + + cosmosTemplate.deleteById(PartitionPerson.class.getSimpleName(), + TEST_PERSON.getId(), new PartitionKey(TEST_PERSON.getZipCode())).block(); + StepVerifier.create(cosmosTemplate.findAll(PartitionPerson.class)) + .expectNext(TEST_PERSON_2) + .verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testDeleteAll() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())).block(); + StepVerifier.create(cosmosTemplate.findAll(PartitionPerson.class)).expectNextCount(2).verifyComplete(); + cosmosTemplate.deleteAll(containerName, PartitionPerson.class).block(); + StepVerifier.create(cosmosTemplate.findAll(PartitionPerson.class)) + .expectNextCount(0) + .verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testCountForPartitionedCollection() { + StepVerifier.create(cosmosTemplate.count(containerName)) + .expectNext((long) 1).verifyComplete(); + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())).block(); + StepVerifier.create(cosmosTemplate.count(containerName)) + .expectNext((long) 2).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testCountForPartitionedCollectionByQuery() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())).block(); + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON_2.getFirstName()), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + StepVerifier.create(cosmosTemplate.count(query, containerName)) + .expectNext((long) 1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testCountIgnoreCaseForPartitionedCollectionByQuery() { + cosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(TEST_PERSON_2.getZipCode())).block(); + final Criteria criteriaIgnoreCase = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList(TEST_PERSON_2.getFirstName().toUpperCase()), Part.IgnoreCaseType.ALWAYS); + final CosmosQuery queryIgnoreCase = new CosmosQuery(criteriaIgnoreCase); + StepVerifier.create(cosmosTemplate.count(queryIgnoreCase, containerName)) + .expectNext((long) 1).verifyComplete(); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + Assertions.assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveMultiTenantContainerCosmosFactoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveMultiTenantContainerCosmosFactoryIT.java new file mode 100644 index 000000000000..17b2ce4b97d2 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveMultiTenantContainerCosmosFactoryIT.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosAsyncDatabase; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.CosmosException; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.repository.MultiTenantTestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.test.StepVerifier; + +import java.util.ArrayList; +import java.util.List; + +import static com.azure.spring.data.cosmos.common.TestConstants.ADDRESSES; +import static com.azure.spring.data.cosmos.common.TestConstants.AGE; +import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_1; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_2; +import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = MultiTenantTestRepositoryConfig.class) +public class ReactiveMultiTenantContainerCosmosFactoryIT { + + private final String testDB1 = "Database1"; + + private final String testContainer1= "Container1"; + private final String testContainer2 = "Container2"; + + private final Person TEST_PERSON_1 = new Person(ID_1, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + private final Person TEST_PERSON_2 = new Person(ID_2, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosConfig cosmosConfig; + @Autowired + private CosmosClientBuilder cosmosClientBuilder; + + private MultiTenantContainerCosmosFactory cosmosFactory; + private ReactiveCosmosTemplate reactiveCosmosTemplate; + private CosmosAsyncClient client; + private CosmosEntityInformation personInfo; + + @Before + public void setUp() throws ClassNotFoundException { + /// Setup + client = CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + cosmosFactory = new MultiTenantContainerCosmosFactory(client, testDB1); + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + + try { + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + } catch (Exception e) { + Assert.fail(); + } + + final MappingCosmosConverter cosmosConverter = new MappingCosmosConverter(mappingContext, null); + reactiveCosmosTemplate = new ReactiveCosmosTemplate(cosmosFactory, cosmosConfig, cosmosConverter, null); + personInfo = new CosmosEntityInformation<>(Person.class); + } + + @Test + public void testGetContainerFunctionality() { + // Create testContainer1 and add TEST_PERSON_1 to it + cosmosFactory.manuallySetContainerName = testContainer1; + reactiveCosmosTemplate.createContainerIfNotExists(personInfo).block(); + reactiveCosmosTemplate.deleteAll(personInfo.getContainerName(), Person.class).block(); + assertThat(cosmosFactory.overrideContainerName()).isEqualTo(testContainer1); + reactiveCosmosTemplate.insert(TEST_PERSON_1, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_1))).block(); + + // Create testContainer1 and add TEST_PERSON_2 to it + cosmosFactory.manuallySetContainerName = testContainer2; + reactiveCosmosTemplate.createContainerIfNotExists(personInfo).block(); + reactiveCosmosTemplate.deleteAll(personInfo.getContainerName(), Person.class).block(); + assertThat(cosmosFactory.overrideContainerName()).isEqualTo(testContainer2); + reactiveCosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))).block(); + + // Check that testContainer2 has the correct contents + List expectedResultsContainer2 = new ArrayList<>(); + expectedResultsContainer2.add(TEST_PERSON_2); + Flux fluxDB2 = reactiveCosmosTemplate.findAll(personInfo.getContainerName(), Person.class); + StepVerifier.create(fluxDB2).expectNextCount(1).verifyComplete(); + List resultDB2 = new ArrayList<>(); + fluxDB2.toIterable().forEach(resultDB2::add); + Assert.assertEquals(expectedResultsContainer2, resultDB2); + + // Check that testContainer1 has the correct contents + cosmosFactory.manuallySetContainerName = testContainer1; + List expectedResultsContainer1 = new ArrayList<>(); + expectedResultsContainer1.add(TEST_PERSON_1); + Flux fluxDB1 = reactiveCosmosTemplate.findAll(personInfo.getContainerName(), Person.class); + StepVerifier.create(fluxDB1).expectNextCount(1).verifyComplete(); + List resultDB1 = new ArrayList<>(); + fluxDB1.toIterable().forEach(resultDB1::add); + Assert.assertEquals(expectedResultsContainer1, resultDB1); + + //Cleanup + deleteDatabaseIfExists(testDB1); + } + + private void deleteDatabaseIfExists(String dbName) { + CosmosAsyncDatabase database = client.getDatabase(dbName); + try { + database.delete().block(); + } catch (CosmosException ex) { + assertEquals(ex.getStatusCode(), 404); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveMultiTenantDBCosmosFactoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveMultiTenantDBCosmosFactoryIT.java new file mode 100644 index 000000000000..a865e65a1a07 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/ReactiveMultiTenantDBCosmosFactoryIT.java @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosAsyncDatabase; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.CosmosException; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.repository.MultiTenantTestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.test.StepVerifier; + +import java.util.ArrayList; +import java.util.List; + +import static com.azure.spring.data.cosmos.common.TestConstants.ADDRESSES; +import static com.azure.spring.data.cosmos.common.TestConstants.AGE; +import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_1; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_2; +import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = MultiTenantTestRepositoryConfig.class) +public class ReactiveMultiTenantDBCosmosFactoryIT { + + private final String testDB1 = "Database1"; + private final String testDB2 = "Database2"; + + private final Person TEST_PERSON_1 = new Person(ID_1, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + private final Person TEST_PERSON_2 = new Person(ID_2, FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private ApplicationContext applicationContext; + @Autowired + private CosmosConfig cosmosConfig; + @Autowired + private CosmosClientBuilder cosmosClientBuilder; + + private MultiTenantDBCosmosFactory cosmosFactory; + private ReactiveCosmosTemplate reactiveCosmosTemplate; + private CosmosAsyncClient client; + private CosmosEntityInformation personInfo; + + @Before + public void setUp() throws ClassNotFoundException { + /// Setup + client = CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + cosmosFactory = new MultiTenantDBCosmosFactory(client, testDB1); + final CosmosMappingContext mappingContext = new CosmosMappingContext(); + + try { + mappingContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + } catch (Exception e) { + Assert.fail(); + } + + final MappingCosmosConverter cosmosConverter = new MappingCosmosConverter(mappingContext, null); + reactiveCosmosTemplate = new ReactiveCosmosTemplate(cosmosFactory, cosmosConfig, cosmosConverter, null); + personInfo = new CosmosEntityInformation<>(Person.class); + } + + @Test + public void testGetDatabaseFunctionality() { + // Create DB1 and add TEST_PERSON_1 to it + reactiveCosmosTemplate.createContainerIfNotExists(personInfo).block(); + reactiveCosmosTemplate.deleteAll(personInfo.getContainerName(), Person.class).block(); + assertThat(cosmosFactory.getDatabaseName()).isEqualTo(testDB1); + reactiveCosmosTemplate.insert(TEST_PERSON_1, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_1))).block(); + + // Create DB2 and add TEST_PERSON_2 to it + cosmosFactory.manuallySetDatabaseName = testDB2; + reactiveCosmosTemplate.createContainerIfNotExists(personInfo).block(); + reactiveCosmosTemplate.deleteAll(personInfo.getContainerName(), Person.class).block(); + assertThat(cosmosFactory.getDatabaseName()).isEqualTo(testDB2); + reactiveCosmosTemplate.insert(TEST_PERSON_2, new PartitionKey(personInfo.getPartitionKeyFieldValue(TEST_PERSON_2))).block(); + + // Check that DB2 has the correct contents + List expectedResultsDB2 = new ArrayList<>(); + expectedResultsDB2.add(TEST_PERSON_2); + Flux fluxDB2 = reactiveCosmosTemplate.findAll(personInfo.getContainerName(), Person.class); + StepVerifier.create(fluxDB2).expectNextCount(1).verifyComplete(); + List resultDB2 = new ArrayList<>(); + fluxDB2.toIterable().forEach(resultDB2::add); + assertEquals(expectedResultsDB2, resultDB2); + + // Check that DB1 has the correct contents + cosmosFactory.manuallySetDatabaseName = testDB1; + List expectedResultsDB1 = new ArrayList<>(); + expectedResultsDB1.add(TEST_PERSON_1); + Flux fluxDB1 = reactiveCosmosTemplate.findAll(personInfo.getContainerName(), Person.class); + StepVerifier.create(fluxDB1).expectNextCount(1).verifyComplete(); + List resultDB1 = new ArrayList<>(); + fluxDB1.toIterable().forEach(resultDB1::add); + assertEquals(expectedResultsDB1, resultDB1); + + //Cleanup + deleteDatabaseIfExists(testDB1); + deleteDatabaseIfExists(testDB2); + } + + private void deleteDatabaseIfExists(String dbName) { + CosmosAsyncDatabase database = client.getDatabase(dbName); + try { + database.delete().block(); + } catch (CosmosException ex) { + assertEquals(ex.getStatusCode(), 404); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventErrorIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventErrorIT.java new file mode 100644 index 000000000000..dbcaf04c9a6a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventErrorIT.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.AddressRepository; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.ClassRule; +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 static com.azure.spring.data.cosmos.domain.Address.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {TestRepositoryConfig.class, ErrorEventListenerConfig.class}) +public class ApplicationContextEventErrorIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private AddressRepository repository; + @Autowired + private CosmosTemplate template; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Address.class); + repository.saveAll(Lists.newArrayList(TEST_ADDRESS1_PARTITION1)); + } + + @Test(expected = CosmosAccessException.class) + public void shouldThrowExceptionIfEventListenerThrowsException() { + repository.findById(TEST_ADDRESS1_PARTITION1.getPostalCode(), new PartitionKey(TEST_ADDRESS1_PARTITION1.getCity())); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventErrorReactiveIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventErrorReactiveIT.java new file mode 100644 index 000000000000..6f833a984b6f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventErrorReactiveIT.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveAddressRepository; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.ClassRule; +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 static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION1; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {TestRepositoryConfig.class, ErrorEventListenerConfig.class}) +public class ApplicationContextEventErrorReactiveIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private ReactiveAddressRepository repository; + @Autowired + private CosmosTemplate template; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Address.class); + repository.saveAll(Lists.newArrayList(TEST_ADDRESS1_PARTITION1)).collectList().block(); + } + + @Test(expected = CosmosAccessException.class) + public void shouldThrowExceptionIfEventListenerThrowsException() { + repository.findById(TEST_ADDRESS1_PARTITION1.getPostalCode(), new PartitionKey(TEST_ADDRESS1_PARTITION1.getCity())).block(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventIT.java new file mode 100644 index 000000000000..d9829c58ff2d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventIT.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.AddressRepository; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.ClassRule; +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.ArrayList; +import java.util.List; + +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION1; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION2; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS2_PARTITION1; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS4_PARTITION3; +import static org.assertj.core.api.Assertions.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ApplicationContextEventIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private AddressRepository repository; + @Autowired + private CosmosTemplate template; + @Autowired + private SimpleCosmosMappingEventListener simpleCosmosMappingEventListener; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Address.class); + repository.saveAll(Lists.newArrayList(TEST_ADDRESS1_PARTITION1, TEST_ADDRESS1_PARTITION2, + TEST_ADDRESS2_PARTITION1)); + simpleCosmosMappingEventListener.onAfterLoadEvents = new ArrayList<>(); + } + + @Test + public void shouldPublishAfterLoadEventOnFindById() { + repository.findById(TEST_ADDRESS1_PARTITION1.getPostalCode(), new PartitionKey(TEST_ADDRESS1_PARTITION1.getCity())); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents).hasSize(1); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(0).getContainerName()).isEqualTo("Address"); + } + + @Test + public void shouldPublishAfterLoadEventOnFindAll() { + Iterable
addresses = repository.findAll(); + + //actual Iterable is a BlockingIterable so we need to use it for processing to occur + addresses.iterator().forEachRemaining(Address::getCity); + + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents).hasSize(3); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(0).getContainerName()).isEqualTo("Address"); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(1).getContainerName()).isEqualTo("Address"); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(2).getContainerName()).isEqualTo("Address"); + } + + @Test + public void shouldPublishAfterLoadEventForCustomQueries() { + List cities = new ArrayList<>(); + cities.add(TEST_ADDRESS1_PARTITION1.getCity()); + Iterable
addresses = repository.findByCityIn(cities); + + //actual Iterable is a BlockingIterable so we need to use it for processing to occur + addresses.iterator().forEachRemaining(Address::getCity); + + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents).hasSize(2); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(0).getContainerName()).isEqualTo("Address"); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(1).getContainerName()).isEqualTo("Address"); + } + + @Test + public void shouldPublishAfterLoadEventForAnnotatedCustomQueries() { + repository.annotatedFindListByCity(TEST_ADDRESS1_PARTITION1.getCity()); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents).hasSize(2); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(0).getContainerName()).isEqualTo("Address"); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(1).getContainerName()).isEqualTo("Address"); + } + + @Test + public void shouldPublishAfterLoadEventForPagedQueries() { + repository.annotatedFindByCity(TEST_ADDRESS1_PARTITION1.getCity(), new CosmosPageRequest(0, 10, null)); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents).hasSize(2); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(0).getContainerName()).isEqualTo("Address"); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(1).getContainerName()).isEqualTo("Address"); + } + + @Test + public void shouldNotPublishAfterLoadEventForInserts() { + repository.save(TEST_ADDRESS4_PARTITION3); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.isEmpty()).isTrue(); + } + + @Test + public void shouldNotPublishAfterLoadEventForUpdates() { + repository.save(new Address(TEST_ADDRESS1_PARTITION1.getPostalCode(), TestConstants.STREET_0, TEST_ADDRESS1_PARTITION1.getCity())); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.isEmpty()).isTrue(); + } + + @Test + public void shouldNotPublishAfterLoadEventForDeletes() { + repository.delete(TEST_ADDRESS1_PARTITION1); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.isEmpty()).isTrue(); + } + + @Test + public void shouldNotPublishAfterLoadEventForCustomDeleteQuery() { + repository.deleteByCity(TEST_ADDRESS1_PARTITION1.getCity()); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.isEmpty()).isTrue(); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventReactiveIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventReactiveIT.java new file mode 100644 index 000000000000..2a37ab740c01 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ApplicationContextEventReactiveIT.java @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveAddressRepository; +import org.assertj.core.util.Lists; +import org.junit.Before; +import org.junit.ClassRule; +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.ArrayList; +import java.util.List; + +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION1; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION2; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS2_PARTITION1; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS4_PARTITION3; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ApplicationContextEventReactiveIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private ReactiveAddressRepository repository; + @Autowired + private CosmosTemplate template; + @Autowired + private SimpleCosmosMappingEventListener simpleCosmosMappingEventListener; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Address.class); + repository.saveAll(Lists.newArrayList(TEST_ADDRESS1_PARTITION1, TEST_ADDRESS1_PARTITION2, + TEST_ADDRESS2_PARTITION1)).collectList().block(); + simpleCosmosMappingEventListener.onAfterLoadEvents = new ArrayList<>(); + } + + @Test + public void shouldPublishAfterLoadEventOnFindById() { + repository.findById(TEST_ADDRESS1_PARTITION1.getPostalCode(), new PartitionKey(TEST_ADDRESS1_PARTITION1.getCity())).block(); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents).hasSize(1); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(0).getContainerName()).isEqualTo("Address"); + } + + @Test + public void shouldPublishAfterLoadEventOnFindAll() { + repository.findAll().collectList().block(); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents).hasSize(3); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(0).getContainerName()).isEqualTo("Address"); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(1).getContainerName()).isEqualTo("Address"); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(2).getContainerName()).isEqualTo("Address"); + } + + @Test + public void shouldPublishAfterLoadEventForCustomQueries() { + List cities = new ArrayList<>(); + cities.add(TEST_ADDRESS1_PARTITION1.getCity()); + repository.findByCityIn(cities).collectList().block(); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents).hasSize(2); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(0).getContainerName()).isEqualTo("Address"); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(1).getContainerName()).isEqualTo("Address"); + } + + @Test + public void shouldPublishAfterLoadEventForAnnotatedCustomQueries() { + repository.annotatedFindListByCity(TEST_ADDRESS1_PARTITION1.getCity()).collectList().block(); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents).hasSize(2); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(0).getContainerName()).isEqualTo("Address"); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.get(1).getContainerName()).isEqualTo("Address"); + } + + @Test + public void shouldNotPublishAfterLoadEventForInserts() { + repository.save(TEST_ADDRESS4_PARTITION3).block(); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.isEmpty()).isTrue(); + } + + @Test + public void shouldNotPublishAfterLoadEventForUpdates() { + repository.save(new Address(TEST_ADDRESS1_PARTITION1.getPostalCode(), TestConstants.STREET_0, TEST_ADDRESS1_PARTITION1.getCity())).block(); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.isEmpty()).isTrue(); + } + + @Test + public void shouldNotPublishAfterLoadEventForDeletes() { + repository.delete(TEST_ADDRESS1_PARTITION1).block(); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.isEmpty()).isTrue(); + } + + @Test + public void shouldNotPublishAfterLoadEventForCustomDeleteQuery() { + repository.deleteByCity(TEST_ADDRESS1_PARTITION1.getCity()).block(); + assertThat(simpleCosmosMappingEventListener.onAfterLoadEvents.isEmpty()).isTrue(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ErrorEventListenerConfig.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ErrorEventListenerConfig.java new file mode 100644 index 000000000000..eb3d878d813d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ErrorEventListenerConfig.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import org.springframework.context.annotation.Bean; + +public class ErrorEventListenerConfig { + + @Bean + ThrowErrorEventListener throwErrorEventListener() { + return new ThrowErrorEventListener(); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/SimpleCosmosMappingEventListener.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/SimpleCosmosMappingEventListener.java new file mode 100644 index 000000000000..47e06ff21ce6 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/SimpleCosmosMappingEventListener.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import java.util.ArrayList; + +public class SimpleCosmosMappingEventListener extends AbstractCosmosEventListener { + + public ArrayList> onAfterLoadEvents = new ArrayList<>(); + + @Override + public void onAfterLoad(AfterLoadEvent event) { + onAfterLoadEvents.add(event); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ThrowErrorEventListener.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ThrowErrorEventListener.java new file mode 100644 index 000000000000..ba1a67d44a17 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/ThrowErrorEventListener.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +public class ThrowErrorEventListener extends AbstractCosmosEventListener { + + @Override + public void onAfterLoad(AfterLoadEvent event) { + throw new RuntimeException(); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Address.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Address.java new file mode 100644 index 000000000000..a7a91bba1ebf --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Address.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container() +public class Address { + + public static final Address TEST_ADDRESS1_PARTITION1 = new Address( + TestConstants.POSTAL_CODE, TestConstants.STREET, TestConstants.CITY); + public static final Address TEST_ADDRESS2_PARTITION1 = new Address( + TestConstants.POSTAL_CODE_0, TestConstants.STREET_0, TestConstants.CITY); + public static final Address TEST_ADDRESS1_PARTITION2 = new Address( + TestConstants.POSTAL_CODE_1, TestConstants.STREET_1, TestConstants.CITY_0); + public static final Address TEST_ADDRESS4_PARTITION3 = new Address( + TestConstants.POSTAL_CODE, TestConstants.STREET_2, TestConstants.CITY_1); + + @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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AuditableEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AuditableEntity.java new file mode 100644 index 000000000000..4da7049e08b1 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AuditableEntity.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.annotation.Version; + +import java.time.OffsetDateTime; + +@Container +public class AuditableEntity { + + @Id + String id; + @Version + private String _etag; + @CreatedBy + private String createdBy; + @CreatedDate + private OffsetDateTime createdDate; + @LastModifiedBy + private String lastModifiedBy; + @LastModifiedDate + private OffsetDateTime lastModifiedByDate; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String get_etag() { + return _etag; + } + + public void set_etag(String _etag) { + this._etag = _etag; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public OffsetDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(OffsetDateTime createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public OffsetDateTime getLastModifiedByDate() { + return lastModifiedByDate; + } + + public void setLastModifiedByDate(OffsetDateTime lastModifiedByDate) { + this.lastModifiedByDate = lastModifiedByDate; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AuditableIdGeneratedEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AuditableIdGeneratedEntity.java new file mode 100644 index 000000000000..788de92f41c3 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AuditableIdGeneratedEntity.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.GeneratedValue; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.OffsetDateTime; + +@Container +public class AuditableIdGeneratedEntity { + + @Id + @GeneratedValue + String id; + @CreatedBy + private String createdBy; + @CreatedDate + private OffsetDateTime createdDate; + @LastModifiedBy + private String lastModifiedBy; + @LastModifiedDate + private OffsetDateTime lastModifiedByDate; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public OffsetDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(OffsetDateTime createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public OffsetDateTime getLastModifiedByDate() { + return lastModifiedByDate; + } + + public void setLastModifiedByDate(OffsetDateTime lastModifiedByDate) { + this.lastModifiedByDate = lastModifiedByDate; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AutoScaleSample.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AutoScaleSample.java new file mode 100644 index 000000000000..67936ab567f9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/AutoScaleSample.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.mapping.Container; + +@Container(ru = TestConstants.AUTOSCALE_MAX_THROUGHPUT, autoScale = true) +public class AutoScaleSample { + private String id; + + public AutoScaleSample() { + } + + public AutoScaleSample(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/ComplexIndexPolicyEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/ComplexIndexPolicyEntity.java new file mode 100644 index 000000000000..fbe3a2c5caed --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/ComplexIndexPolicyEntity.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.CompositeIndex; +import com.azure.spring.data.cosmos.core.mapping.CompositeIndexPath; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import org.springframework.data.annotation.Id; + +@Container +@CosmosIndexingPolicy( + includePaths = {"/field/?"}, + excludePaths = {"/*", "/\"_etag\"/?"}, + compositeIndexes = { + @CompositeIndex(paths = { + @CompositeIndexPath(path = "/compositeField1"), + @CompositeIndexPath(path = "/compositeField2") + }) + } +) +public class ComplexIndexPolicyEntity { + + @Id + String id; + + String field; + + String compositeField1; + + String compositeField2; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getCompositeField1() { + return compositeField1; + } + + public void setCompositeField1(String compositeField1) { + this.compositeField1 = compositeField1; + } + + public String getCompositeField2() { + return compositeField2; + } + + public void setCompositeField2(String compositeField2) { + this.compositeField2 = compositeField2; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/CompositeIndexEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/CompositeIndexEntity.java new file mode 100644 index 000000000000..5a605b3b7fca --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/CompositeIndexEntity.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.cosmos.models.CompositePathSortOrder; +import com.azure.spring.data.cosmos.core.mapping.CompositeIndex; +import com.azure.spring.data.cosmos.core.mapping.CompositeIndexPath; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import org.springframework.data.annotation.Id; + +@Container +@CosmosIndexingPolicy(compositeIndexes = { + @CompositeIndex(paths = { + @CompositeIndexPath(path = "/fieldOne"), + @CompositeIndexPath(path = "/fieldTwo") + }), + @CompositeIndex(paths = { + @CompositeIndexPath(path = "/fieldThree", order = CompositePathSortOrder.DESCENDING), + @CompositeIndexPath(path = "/fieldFour", order = CompositePathSortOrder.DESCENDING) + }) +}) +public class CompositeIndexEntity { + + @Id + String id; + + String fieldOne; + String fieldTwo; + String fieldThree; + String fieldFour; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFieldOne() { + return fieldOne; + } + + public void setFieldOne(String fieldOne) { + this.fieldOne = fieldOne; + } + + public String getFieldTwo() { + return fieldTwo; + } + + public void setFieldTwo(String fieldTwo) { + this.fieldTwo = fieldTwo; + } + + public String getFieldThree() { + return fieldThree; + } + + public void setFieldThree(String fieldThree) { + this.fieldThree = fieldThree; + } + + public String getFieldFour() { + return fieldFour; + } + + public void setFieldFour(String fieldFour) { + this.fieldFour = fieldFour; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Contact.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Contact.java new file mode 100644 index 000000000000..5b8bc513dfd3 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Contact.java @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container() +public class Contact { + @Id + private String logicId; + + private String title; + + private int intValue; + + private boolean isActive; + + public Contact() { + } + + public Contact(String logicId, String title) { + this.logicId = logicId; + this.title = title; + } + + public Contact(final String logicId, final String title, final int intValue, boolean isActive) { + this.logicId = logicId; + this.title = title; + this.intValue = intValue; + this.isActive = isActive; + } + + 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; + } + + /** + * Getter for property 'status'. + * + * @return Value for property 'status'. + */ + public boolean isActive() { + return isActive; + } + + /** + * Setter for property 'status'. + * + * @param active Value to set for property 'status'. + */ + public void setActive(final boolean active) { + this.isActive = active; + } + + /** + * Getter for property 'value'. + * + * @return Value for property 'value'. + */ + public int getIntValue() { + return intValue; + } + + /** + * Setter for property 'value'. + * + * @param intValue Value to set for property 'value'. + */ + public void setIntValue(final int intValue) { + this.intValue = intValue; + } + + @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 + + '\'' + + ", value='" + + intValue + + '\'' + + ", status='" + + isActive + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Course.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Course.java new file mode 100644 index 000000000000..9bf469da8a11 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Course.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container +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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/CourseWithEtag.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/CourseWithEtag.java new file mode 100644 index 000000000000..da992e815c75 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/CourseWithEtag.java @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; + +import java.util.Objects; + +@Container +public class CourseWithEtag { + + @Id + private String courseId; + private String name; + @PartitionKey + private String department; + @Version + private String etag; + + public CourseWithEtag(String courseId, String name, String department) { + this.courseId = courseId; + this.name = name; + this.department = department; + } + + public CourseWithEtag() { + } + + 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; + } + + public String getEtag() { + return this.etag; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CourseWithEtag course = (CourseWithEtag) 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 + + '\'' + + ", etag='" + + etag + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Customer.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Customer.java new file mode 100644 index 000000000000..a1395e5be571 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Customer.java @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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() { + } + + 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() { + } + + 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/GenIdEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/GenIdEntity.java new file mode 100644 index 000000000000..5fae624e6444 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/GenIdEntity.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.GeneratedValue; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +public class GenIdEntity { + + @Id + @GeneratedValue + private String id; + + private String foo; + + public GenIdEntity() { + } + + public GenIdEntity(String id, String foo) { + this.id = id; + this.foo = foo; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GenIdEntity that = (GenIdEntity) o; + return Objects.equals(id, that.id) && Objects.equals(foo, that.foo); + } + + @Override + public int hashCode() { + return Objects.hash(id, foo); + } + + @Override + public String toString() { + return "GenIdEntity{" + + "id='" + + id + + '\'' + + ", foo='" + + foo + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Importance.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Importance.java new file mode 100644 index 000000000000..8d4731c396cf --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Importance.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +public enum Importance { + HIGH, LOW, NORMAL; +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IndexPolicyEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IndexPolicyEntity.java new file mode 100644 index 000000000000..0035cf2b3343 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IndexPolicyEntity.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import org.springframework.data.annotation.Id; + +@Container +@CosmosIndexingPolicy +public class IndexPolicyEntity { + + @Id + String id; + + String field; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IndexPolicyOverwriteEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IndexPolicyOverwriteEntity.java new file mode 100644 index 000000000000..b1ca09c2d58d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IndexPolicyOverwriteEntity.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import org.springframework.data.annotation.Id; + +@Container +@CosmosIndexingPolicy( + overwritePolicy = true, + includePaths = {"/\"_etag\"/?"}, + excludePaths = {"/*"} +) +public class IndexPolicyOverwriteEntity { + + @Id + String id; + + String field; + + String compositeField1; + + String compositeField2; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getCompositeField1() { + return compositeField1; + } + + public void setCompositeField1(String compositeField1) { + this.compositeField1 = compositeField1; + } + + public String getCompositeField2() { + return compositeField2; + } + + public void setCompositeField2(String compositeField2) { + this.compositeField2 = compositeField2; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IntegerIdDomain.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IntegerIdDomain.java new file mode 100644 index 000000000000..1979fab6702e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/IntegerIdDomain.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container +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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomain.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomain.java new file mode 100644 index 000000000000..8c4021132c48 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomain.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container +public class LongIdDomain { + + @Id + private Long number; + + private String name; + + public LongIdDomain(Long number, String name) { + this.number = number; + this.name = name; + } + + public LongIdDomain() { + } + + public Long getNumber() { + return number; + } + + public void setNumber(Long 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; + } + LongIdDomain that = (LongIdDomain) 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 "LongIdDomain{" + + "number=" + + number + + ", name='" + + name + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomainPartition.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomainPartition.java new file mode 100644 index 000000000000..081ebd7c0ed5 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomainPartition.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container +public class LongIdDomainPartition { + + @Id + private Long number; + + @PartitionKey + private String name; + + public LongIdDomainPartition(Long number, String name) { + this.number = number; + this.name = name; + } + + public LongIdDomainPartition() { + } + + public Long getNumber() { + return number; + } + + public void setNumber(Long 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; + } + LongIdDomainPartition that = (LongIdDomainPartition) 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 "LongIdDomain{" + + "number=" + + number + + ", name='" + + name + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Memo.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Memo.java new file mode 100644 index 000000000000..b46578d3402a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Memo.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; + +import java.util.Date; +import java.util.Objects; + +/** + * For testing date and enum purpose + */ +@Container() +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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedEntity.java new file mode 100644 index 000000000000..fcd00e2bb4f3 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedEntity.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import java.util.Objects; + +public class NestedEntity { + private String nestedPartitionKey; + + public NestedEntity(String nestedPartitionKey) { + this.nestedPartitionKey = nestedPartitionKey; + } + + public NestedEntity() { + + } + + public String getNestedPartitionKey() { + return nestedPartitionKey; + } + + public void setNestedPartitionKey(String nestedPartitionKey) { + this.nestedPartitionKey = nestedPartitionKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedEntity that = (NestedEntity) o; + return Objects.equals(nestedPartitionKey, that.nestedPartitionKey); + } + + @Override + public int hashCode() { + return Objects.hash(nestedPartitionKey); + } + + @Override + public String toString() { + return "NestedEntity{" + + "nestedPartitionKey='" + nestedPartitionKey + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedPartitionKeyEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedPartitionKeyEntity.java new file mode 100644 index 000000000000..a299209e2d71 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedPartitionKeyEntity.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.GeneratedValue; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container(containerName = "nested-partition-key", partitionKeyPath = "/nestedEntity/nestedPartitionKey") +public class NestedPartitionKeyEntity { + + @Id + @GeneratedValue + private String id; + + private NestedEntity nestedEntity; + + public NestedPartitionKeyEntity(String id, NestedEntity nestedEntity) { + this.id = id; + this.nestedEntity = nestedEntity; + } + + public NestedPartitionKeyEntity() { + + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public NestedEntity getNestedEntity() { + return nestedEntity; + } + + public void setNestedEntity(NestedEntity nestedEntity) { + this.nestedEntity = nestedEntity; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedPartitionKeyEntity that = (NestedPartitionKeyEntity) o; + return Objects.equals(id, that.id) && Objects.equals(nestedEntity, that.nestedEntity); + } + + @Override + public int hashCode() { + return Objects.hash(id, nestedEntity); + } + + @Override + public String toString() { + return "NestedPartitionKeyEntity{" + + "id='" + id + '\'' + + ", nestedEntity=" + nestedEntity + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedPartitionKeyEntityWithGeneratedValue.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedPartitionKeyEntityWithGeneratedValue.java new file mode 100644 index 000000000000..cd74099241df --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/NestedPartitionKeyEntityWithGeneratedValue.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.GeneratedValue; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container(containerName = "nested-partition-key-with-generated-value", partitionKeyPath = "/nestedEntity/nestedPartitionKey") +public class NestedPartitionKeyEntityWithGeneratedValue { + + @Id + @GeneratedValue + private String id; + + private NestedEntity nestedEntity; + + public NestedPartitionKeyEntityWithGeneratedValue(String id, NestedEntity nestedEntity) { + this.id = id; + this.nestedEntity = nestedEntity; + } + + public NestedPartitionKeyEntityWithGeneratedValue() { + + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public NestedEntity getNestedEntity() { + return nestedEntity; + } + + public void setNestedEntity(NestedEntity nestedEntity) { + this.nestedEntity = nestedEntity; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedPartitionKeyEntityWithGeneratedValue that = (NestedPartitionKeyEntityWithGeneratedValue) o; + return Objects.equals(id, that.id) && Objects.equals(nestedEntity, that.nestedEntity); + } + + @Override + public int hashCode() { + return Objects.hash(id, nestedEntity); + } + + @Override + public String toString() { + return "NestedPartitionKeyEntityWithGeneratedValue{" + + "id='" + id + '\'' + + ", nestedEntity=" + nestedEntity + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PageableMemo.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PageableMemo.java new file mode 100644 index 000000000000..5f4d734281ab --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PageableMemo.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; + +import java.util.Date; +import java.util.Objects; + +/** + * For testing date and enum purpose + */ +@Container() +public class PageableMemo { + private String id; + private String message; + private Date date; + private Importance importance; + + public PageableMemo() { + } + + 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PageablePerson.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PageablePerson.java new file mode 100644 index 000000000000..7c7e96313f34 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PageablePerson.java @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Version; + +import java.util.List; +import java.util.Objects; + +@Container() +@CosmosIndexingPolicy() +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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PartitionPerson.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PartitionPerson.java new file mode 100644 index 000000000000..752c4a86e361 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PartitionPerson.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; + +import java.util.List; +import java.util.Objects; + +@Container() +public class PartitionPerson { + + private String id; + + private String firstName; + + @PartitionKey + private Integer zipCode; + + private List hobbies; + + private List
shippingAddresses; + + public PartitionPerson() { + } + + public PartitionPerson(String id, String firstName, Integer zipCode, List hobbies, List
shippingAddresses) { + this.id = id; + this.firstName = firstName; + this.zipCode = zipCode; + 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 Integer getZipCode() { + return zipCode; + } + + public void setZipCode(Integer zipCode) { + this.zipCode = zipCode; + } + + 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(zipCode, that.zipCode) + && Objects.equals(hobbies, that.hobbies) + && Objects.equals(shippingAddresses, that.shippingAddresses); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, zipCode, hobbies, shippingAddresses); + } + + @Override + public String toString() { + return "PartitionPerson{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + zipCode + + '\'' + + ", hobbies=" + + hobbies + + ", shippingAddresses=" + + shippingAddresses + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersistableEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersistableEntity.java new file mode 100644 index 000000000000..707472095158 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersistableEntity.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.domain.Persistable; + +import java.util.Objects; + +@Container() +public class PersistableEntity implements Persistable { + + @Id + private String id; + @PartitionKey + private String partitionKey; + @Version + private String version; + + public PersistableEntity() { + } + + public PersistableEntity(String id, String partitionKey) { + this(id, partitionKey, null); + } + + public PersistableEntity(String id, String partitionKey, String version) { + this.id = id; + this.partitionKey = partitionKey; + this.version = version; + } + + @Override + public String getId() { + return id; + } + + @Override + public boolean isNew() { + return version == null; + } + + public String getPartitionKey() { + return partitionKey; + } + + public String getVersion() { + return version; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PersistableEntity that = (PersistableEntity) o; + return Objects.equals(id, that.id) && Objects.equals(partitionKey, that.partitionKey); + } + + @Override + public int hashCode() { + return Objects.hash(id, partitionKey); + } + + @Override + public String toString() { + return "PersistableEntity{" + + "id='" + id + '\'' + + ", partitionKey='" + partitionKey + '\'' + + ", version='" + version + '\'' + + '}'; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Person.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Person.java new file mode 100644 index 000000000000..251f8edd74ab --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Person.java @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Version; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Container() +@CosmosIndexingPolicy() +public class Person { + private String id; + private String firstName; + + @PartitionKey + private String lastName; + private List hobbies; + private List
shippingAddresses; + private Integer age; + private Map passportIdsByCountry; + @Version + private String _etag; + + public Person(String id, String firstName, String lastName, List hobbies, List
shippingAddresses, + Integer age, Map passportIDsByCountry) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.hobbies = hobbies; + this.shippingAddresses = shippingAddresses; + this.age = age; + this.passportIdsByCountry = passportIDsByCountry; + } + + 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; + } + + public Integer getAge() { + return this.age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Map getPassportIdsByCountry() { + return passportIdsByCountry; + } + + public void setPassportIdsByCountry(Map passportIdsByCountry) { + this.passportIdsByCountry = passportIdsByCountry; + } + + @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) + && Objects.equals(age, person.age) + && Objects.equals(passportIdsByCountry, person.passportIdsByCountry); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName, hobbies, shippingAddresses, age, passportIdsByCountry); + } + + @Override + public String toString() { + return "Person{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + ", age=" + + age + + ", hobbies=" + + hobbies + + ", shippingAddresses=" + + shippingAddresses + + ", passportIdsByCountry=" + + passportIdsByCountry + + ", _etag='" + + _etag + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersonCrossPartition.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersonCrossPartition.java new file mode 100644 index 000000000000..90d6d7ce682f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersonCrossPartition.java @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Version; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Container(ru = TestConstants.MULTI_PARTITION_THROUGHPUT, autoScale = true) +@CosmosIndexingPolicy() +public class PersonCrossPartition { + private String id; + private String firstName; + + @PartitionKey + private String lastName; + private List hobbies; + private List
shippingAddresses; + private Integer age; + private Map passportIdsByCountry; + @Version + private String _etag; + + public PersonCrossPartition(String id, String firstName, String lastName, List hobbies, List
shippingAddresses, + Integer age, Map passportIDsByCountry) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.hobbies = hobbies; + this.shippingAddresses = shippingAddresses; + this.age = age; + this.passportIdsByCountry = passportIDsByCountry; + } + + public PersonCrossPartition() { + } + + 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; + } + + public Integer getAge() { + return this.age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Map getPassportIdsByCountry() { + return passportIdsByCountry; + } + + public void setPassportIdsByCountry(Map passportIdsByCountry) { + this.passportIdsByCountry = passportIdsByCountry; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PersonCrossPartition person = (PersonCrossPartition) 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) + && Objects.equals(age, person.age) + && Objects.equals(passportIdsByCountry, person.passportIdsByCountry); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName, hobbies, shippingAddresses, age, passportIdsByCountry); + } + + @Override + public String toString() { + return "Person{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + ", age=" + + age + + ", hobbies=" + + hobbies + + ", shippingAddresses=" + + shippingAddresses + + ", passportIdsByCountry=" + + passportIdsByCountry + + ", _etag='" + + _etag + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersonWithEtag.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersonWithEtag.java new file mode 100644 index 000000000000..a4fcbe0d4b16 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/PersonWithEtag.java @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Version; + +import java.util.List; +import java.util.Objects; + +@Container() +@CosmosIndexingPolicy() +public class PersonWithEtag { + private String id; + private String firstName; + + @PartitionKey + private String lastName; + private List hobbies; + private List
shippingAddresses; + @Version + private String etag; + + public PersonWithEtag(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 PersonWithEtag() { + } + + 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 getEtag() { + return etag; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PersonWithEtag person = (PersonWithEtag) 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Project.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Project.java new file mode 100644 index 000000000000..32df15c106fb --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Project.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container() +@CosmosIndexingPolicy() +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() { + } + + 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Question.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Question.java new file mode 100644 index 000000000000..eb04df85309d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Question.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.cosmos.models.IndexingMode; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; +import java.util.UUID; + +@Container +@CosmosIndexingPolicy(mode = IndexingMode.CONSISTENT) +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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/ReactiveTeacher.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/ReactiveTeacher.java new file mode 100644 index 000000000000..31209f42e5a0 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/ReactiveTeacher.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.cosmos.models.IndexingMode; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Container +@CosmosIndexingPolicy(mode = IndexingMode.CONSISTENT) +public class ReactiveTeacher { + private String id; + private String firstName; + private String lastName; + + public ReactiveTeacher() { + } + + public ReactiveTeacher(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) { + if (null != lastName) { + this.lastName = lastName + "-changed"; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReactiveTeacher teacher = (ReactiveTeacher) o; + return Objects.equals(id, teacher.id) + && Objects.equals(firstName, teacher.firstName) + && Objects.equals(lastName, teacher.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName); + } + + @Override + public String toString() { + return "Teacher{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Role.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Role.java new file mode 100644 index 000000000000..958eb0d74a68 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Role.java @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.cosmos.models.IndexingMode; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@CosmosIndexingPolicy( + mode = IndexingMode.CONSISTENT, + automatic = TestConstants.INDEXING_POLICY_AUTOMATIC, + includePaths = { + TestConstants.INCLUDED_PATH_0, + TestConstants.INCLUDED_PATH_1, + TestConstants.INCLUDED_PATH_2, + }, + excludePaths = { + TestConstants.EXCLUDED_PATH_0, + TestConstants.EXCLUDED_PATH_1, + TestConstants.EXCLUDED_PATH_2, + }) +@Container(containerName = TestConstants.ROLE_COLLECTION_NAME, + autoCreateContainer = false) +public class Role { + @Id + String id; + + @PartitionKey + boolean developer; + + String name; + + String level; + + public Role() { + } + + public Role(String id, boolean developer, String name, String level) { + this.id = id; + this.developer = developer; + 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; + } + + public boolean isDeveloper() { + return developer; + } + + public void setDeveloper(boolean developer) { + this.developer = developer; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Role role = (Role) o; + return developer == role.developer && Objects.equals(id, role.id) + && Objects.equals(name, role.name) && Objects.equals(level, role.level); + } + + @Override + public int hashCode() { + return Objects.hash(id, developer, name, level); + } + + @Override + public String toString() { + return "Role{" + + "id='" + id + '\'' + + ", isDeveloper=" + developer + + ", name='" + name + '\'' + + ", level='" + level + '\'' + + '}'; + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SortedProject.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SortedProject.java new file mode 100644 index 000000000000..116a8f001959 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SortedProject.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container() +@CosmosIndexingPolicy() +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() { + } + + 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SpELBeanStudent.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SpELBeanStudent.java new file mode 100644 index 000000000000..727d5a307fcf --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SpELBeanStudent.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; + +import java.util.Objects; + +@Container(containerName = "#{@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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SpELPropertyStudent.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SpELPropertyStudent.java new file mode 100644 index 000000000000..85c8d8c703e7 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/SpELPropertyStudent.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; + +import java.util.Objects; + +@Container(containerName = "${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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Student.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Student.java new file mode 100644 index 000000000000..3f0ae3dc54ff --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Student.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; + +import java.util.Objects; + +@CosmosIndexingPolicy() +public class Student { + private String id; + private String firstName; + private String lastName; + + public Student() { + } + + 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Teacher.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Teacher.java new file mode 100644 index 000000000000..7acf7225a56e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Teacher.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.cosmos.models.IndexingMode; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Container +@CosmosIndexingPolicy(mode = IndexingMode.CONSISTENT) +public class Teacher { + private String id; + private String firstName; + private String lastName; + + public Teacher() { + } + + public Teacher(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) { + if (null != lastName) { + this.lastName = lastName + "-changed"; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Teacher teacher = (Teacher) o; + return Objects.equals(id, teacher.id) + && Objects.equals(firstName, teacher.firstName) + && Objects.equals(lastName, teacher.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(id, firstName, lastName); + } + + @Override + public String toString() { + return "Teacher{" + + "id='" + + id + + '\'' + + ", firstName='" + + firstName + + '\'' + + ", lastName='" + + lastName + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/TimeToLiveSample.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/TimeToLiveSample.java new file mode 100644 index 000000000000..01064077006a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/TimeToLiveSample.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.mapping.Container; + +@Container(timeToLive = TestConstants.TIME_TO_LIVE) +public class TimeToLiveSample { + private String id; + + public TimeToLiveSample() { + } + + public TimeToLiveSample(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/UUIDIdDomain.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/UUIDIdDomain.java new file mode 100644 index 000000000000..f3b9f8141445 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/UUIDIdDomain.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.Id; + +import java.util.Objects; +import java.util.UUID; + +@Container +public class UUIDIdDomain { + + @Id + private UUID number; + + private String name; + + public UUIDIdDomain(UUID number, String name) { + this.number = number; + this.name = name; + } + + public UUIDIdDomain() { + } + + public UUID getNumber() { + return number; + } + + public void setNumber(UUID 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; + } + UUIDIdDomain that = (UUIDIdDomain) 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 "UUIDIdDomain{" + + "number=" + + number + + ", name='" + + name + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/UniqueKeyPolicyEntity.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/UniqueKeyPolicyEntity.java new file mode 100644 index 000000000000..47ebdaf45d8c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/UniqueKeyPolicyEntity.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosUniqueKey; +import com.azure.spring.data.cosmos.core.mapping.CosmosUniqueKeyPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +@Container +@CosmosUniqueKeyPolicy(uniqueKeys = { + @CosmosUniqueKey(paths = {"/lastName", "/zipCode"}), + @CosmosUniqueKey(paths = {"/city"}) +}) +public class UniqueKeyPolicyEntity { + + @Id + String id; + + @PartitionKey + String firstName; + + String lastName; + String zipCode; + String city; + + public UniqueKeyPolicyEntity(String id, String firstName, String lastName, String zipCode, String city) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.zipCode = zipCode; + this.city = city; + } + + 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 getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/inheritance/Shape.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/inheritance/Shape.java new file mode 100644 index 000000000000..3ea83f1ba91a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/inheritance/Shape.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/inheritance/Square.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/inheritance/Square.java new file mode 100644 index 000000000000..b96d38c4d837 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/inheritance/Square.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/MultiCosmosTemplateIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/MultiCosmosTemplateIT.java new file mode 100644 index 000000000000..02b00ca05833 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/MultiCosmosTemplateIT.java @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.assertj.core.api.Assertions; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Mono; + +import java.lang.reflect.Field; + +import static com.azure.spring.data.cosmos.common.TestConstants.AGE; +import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {TestRepositoryConfig.class, SecondaryTestRepositoryConfig.class}) +public class MultiCosmosTemplateIT { + private static final Person PRIMARY_TEST_PERSON = new Person(TestConstants.ID_1, TestConstants.FIRST_NAME, + TestConstants.LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + private static final Person SECONDARY_TEST_PERSON = new Person(TestConstants.ID_2, TestConstants.NEW_FIRST_NAME, + TestConstants.NEW_LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager primaryCollectionManager = new ReactiveIntegrationTestCollectionManager(); + + @Autowired + @Qualifier("secondaryReactiveCosmosTemplate") + private ReactiveCosmosTemplate secondaryReactiveCosmosTemplate; + @Autowired + @Qualifier("secondaryReactiveCosmosTemplate1") + private ReactiveCosmosTemplate secondaryDiffDatabaseReactiveCosmosTemplate; + @Autowired + @Qualifier("reactiveCosmosTemplate") + private ReactiveCosmosTemplate primaryReactiveCosmosTemplate; + private CosmosEntityInformation personInfo = new CosmosEntityInformation<>(Person.class); + + @Test + public void testPrimaryTemplate() { + primaryCollectionManager.ensureContainersCreatedAndEmpty(primaryReactiveCosmosTemplate, Person.class); + primaryReactiveCosmosTemplate.insert(PRIMARY_TEST_PERSON, + new PartitionKey(personInfo.getPartitionKeyFieldValue(PRIMARY_TEST_PERSON))).block(); + final Mono findById = primaryReactiveCosmosTemplate.findById(PRIMARY_TEST_PERSON.getId(), Person.class); + Assertions.assertThat(findById.block().getFirstName()).isEqualTo(TestConstants.FIRST_NAME); + } + + @Test + public void testSecondaryTemplate() { + secondaryReactiveCosmosTemplate.createContainerIfNotExists(personInfo).block(); + secondaryReactiveCosmosTemplate.insert(SECONDARY_TEST_PERSON, + new PartitionKey(personInfo.getPartitionKeyFieldValue(SECONDARY_TEST_PERSON))).block(); + final Mono findById = secondaryReactiveCosmosTemplate.findById(SECONDARY_TEST_PERSON.getId(), Person.class); + Assertions.assertThat(findById.block().getFirstName()).isEqualTo(TestConstants.NEW_FIRST_NAME); + secondaryReactiveCosmosTemplate.deleteAll(Person.class.getSimpleName(), Person.class).block(); + secondaryReactiveCosmosTemplate.deleteContainer(personInfo.getContainerName()); + } + + @Test + public void testSecondaryTemplateWithDiffDatabase() { + secondaryDiffDatabaseReactiveCosmosTemplate.createContainerIfNotExists(personInfo).block(); + secondaryDiffDatabaseReactiveCosmosTemplate.insert(SECONDARY_TEST_PERSON, + new PartitionKey(personInfo.getPartitionKeyFieldValue(SECONDARY_TEST_PERSON))).block(); + final Mono findById = secondaryDiffDatabaseReactiveCosmosTemplate.findById(SECONDARY_TEST_PERSON.getId(), Person.class); + Assertions.assertThat(findById.block().getFirstName()).isEqualTo(TestConstants.NEW_FIRST_NAME); + secondaryDiffDatabaseReactiveCosmosTemplate.deleteAll(Person.class.getSimpleName(), Person.class).block(); + secondaryDiffDatabaseReactiveCosmosTemplate.deleteContainer(personInfo.getContainerName()); + } + + @Test + public void testSingleCosmosClientForMultipleCosmosTemplate() throws IllegalAccessException { + final Field cosmosFactory = FieldUtils.getDeclaredField(ReactiveCosmosTemplate.class, + "cosmosFactory", true); + CosmosFactory cosmosFactory1 = (CosmosFactory) cosmosFactory.get(secondaryReactiveCosmosTemplate); + CosmosAsyncClient client1 = cosmosFactory1.getCosmosAsyncClient(); + CosmosFactory cosmosFactory2 = (CosmosFactory) cosmosFactory.get(secondaryDiffDatabaseReactiveCosmosTemplate); + CosmosAsyncClient client2 = cosmosFactory2.getCosmosAsyncClient(); + Assertions.assertThat(client1).isEqualTo(client2); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/MultiTenantTestRepositoryConfig.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/MultiTenantTestRepositoryConfig.java new file mode 100644 index 000000000000..e28533a40997 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/MultiTenantTestRepositoryConfig.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.common.ResponseDiagnosticsTestUtils; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.config.AbstractCosmosConfiguration; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.MultiTenantDBCosmosFactory; +import com.azure.spring.data.cosmos.core.mapping.event.SimpleCosmosMappingEventListener; +import com.azure.spring.data.cosmos.repository.config.EnableCosmosRepositories; +import com.azure.spring.data.cosmos.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; + +import java.util.Arrays; +import java.util.Collection; + +@Configuration +@PropertySource(value = { "classpath:application.properties" }) +@EnableCosmosRepositories +@EnableReactiveCosmosRepositories +public class MultiTenantTestRepositoryConfig extends AbstractCosmosConfiguration { + @Value("${cosmos.uri:}") + private String cosmosDbUri; + + @Value("${cosmos.key:}") + private String cosmosDbKey; + + @Value("${cosmos.database:}") + private String database; + + @Value("${cosmos.queryMetricsEnabled}") + private boolean queryMetricsEnabled; + + @Value("${cosmos.maxDegreeOfParallelism}") + private int maxDegreeOfParallelism; + + @Value("${cosmos.maxBufferedItemCount}") + private int maxBufferedItemCount; + + @Value("${cosmos.responseContinuationTokenLimitInKb}") + private int responseContinuationTokenLimitInKb; + + @Bean + public ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils() { + return new ResponseDiagnosticsTestUtils(); + } + + @Bean + public CosmosClientBuilder cosmosClientBuilder() { + return new CosmosClientBuilder() + .key(cosmosDbKey) + .endpoint(cosmosDbUri) + .contentResponseOnWriteEnabled(true); + } + + @Bean + public MultiTenantDBCosmosFactory cosmosFactory(CosmosAsyncClient cosmosAsyncClient) { + return new MultiTenantDBCosmosFactory(cosmosAsyncClient, getDatabaseName()); + } + + @Bean + @Override + public CosmosConfig cosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(queryMetricsEnabled) + .maxDegreeOfParallelism(maxDegreeOfParallelism) + .maxBufferedItemCount(maxBufferedItemCount) + .responseContinuationTokenLimitInKb(responseContinuationTokenLimitInKb) + .responseDiagnosticsProcessor(responseDiagnosticsTestUtils().getResponseDiagnosticsProcessor()) + .build(); + } + + @Override + protected String getDatabaseName() { + return StringUtils.hasText(this.database) ? this.database : TestConstants.DB_NAME; + } + + @Override + protected Collection getMappingBasePackages() { + final Package mappingBasePackage = getClass().getPackage(); + final String entityPackage = "com.azure.spring.data.cosmos.domain"; + return Arrays.asList(mappingBasePackage.getName(), entityPackage); + } + + @Bean + SimpleCosmosMappingEventListener simpleMappingEventListener() { + return new SimpleCosmosMappingEventListener(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/ReactiveUUIDIdDomainRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/ReactiveUUIDIdDomainRepositoryIT.java new file mode 100644 index 000000000000..dd13d14ccf5a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/ReactiveUUIDIdDomainRepositoryIT.java @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.domain.UUIDIdDomain; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.repository.ReactiveUUIDIdDomainRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Before; +import org.junit.ClassRule; +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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveUUIDIdDomainRepositoryIT { + + private static final UUID ID_1 = UUID.randomUUID(); + private static final String NAME_1 = "moary"; + + private static final UUID ID_2 = UUID.randomUUID(); + private static final String NAME_2 = "camille"; + + private static final UUIDIdDomain DOMAIN_1 = new UUIDIdDomain(ID_1, NAME_1); + private static final UUIDIdDomain DOMAIN_2 = new UUIDIdDomain(ID_2, NAME_2); + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); + + @Autowired + private ReactiveCosmosTemplate template; + + @Autowired + private ReactiveUUIDIdDomainRepository repository; + + private CosmosEntityInformation entityInformation; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, UUIDIdDomain.class); + entityInformation = collectionManager.getEntityInformation(UUIDIdDomain.class); + Flux savedAllFlux = this.repository.saveAll(Arrays.asList(DOMAIN_1, DOMAIN_2)); + StepVerifier.create(savedAllFlux).thenConsumeWhile(domain -> true).expectComplete().verify(); + } + + @Test + public void testUUIDIdDomain() { + Mono deletedMono = this.repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + Mono idMono = this.repository.findById(ID_1); + StepVerifier.create(idMono).expectNextCount(0).verifyComplete(); + + Mono saveMono = this.repository.save(DOMAIN_1); + StepVerifier.create(saveMono).expectNext(DOMAIN_1).expectComplete().verify(); + + Mono findIdMono = this.repository.findById(ID_1); + StepVerifier.create(findIdMono).expectNext(DOMAIN_1).expectComplete().verify(); + + Mono deleteMono = this.repository.delete(DOMAIN_1); + StepVerifier.create(deleteMono).verifyComplete(); + + Mono afterDelIdMono = this.repository.findById(ID_1); + StepVerifier.create(afterDelIdMono).expectNextCount(0).verifyComplete(); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidDomain() { + new CosmosEntityInformation(InvalidDomain.class); + } + + @Test + public void testSaveAllAndFindAll() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + Flux savedAllFlux = this.repository.saveAll(Arrays.asList(DOMAIN_1, DOMAIN_2)); + StepVerifier.create(savedAllFlux).expectNextCount(2).verifyComplete(); + + final Flux allFlux = repository.findAll(); + StepVerifier.create(allFlux).expectNextCount(2).verifyComplete(); + } + + @Test + public void testCount() { + Mono countMono = repository.count(); + StepVerifier.create(countMono).expectNext(2L).verifyComplete(); + } + + @Test + public void testDeleteByIdShouldFailIfNothingToDelete() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + final Mono deleteIdMono = repository.deleteById(DOMAIN_1.getNumber(), + new PartitionKey(entityInformation.getPartitionKeyFieldValue(DOMAIN_1))); + StepVerifier.create(deleteIdMono).expectError(CosmosAccessException.class).verify(); + } + + @Test + public void testDelete() { + Mono saveMono = this.repository.save(DOMAIN_1); + StepVerifier.create(saveMono).expectNext(DOMAIN_1).expectComplete().verify(); + + Mono deleteMono = this.repository.delete(DOMAIN_1); + StepVerifier.create(deleteMono).verifyComplete(); + + Mono countMono = repository.count(); + StepVerifier.create(countMono).expectNext(1L).verifyComplete(); + } + + @Test + public void testDeleteShouldFailIfNothingToDelete() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + Mono deleteIdMono = this.repository.delete(DOMAIN_1); + StepVerifier.create(deleteIdMono).expectError(CosmosAccessException.class).verify(); + } + + @Test + public void testDeleteAll() { + Flux savedAllFlux = this.repository.saveAll(Arrays.asList(DOMAIN_1, DOMAIN_2)); + StepVerifier.create(savedAllFlux).expectNextCount(2).verifyComplete(); + + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + Mono countMono = repository.count(); + StepVerifier.create(countMono).expectNext(0L).verifyComplete(); + } + + @Test + public void testExistsById() { + Mono saveMono = this.repository.save(DOMAIN_1); + StepVerifier.create(saveMono).expectNext(DOMAIN_1).expectComplete().verify(); + + Mono booleanMono = this.repository.existsById(DOMAIN_1.getNumber()); + StepVerifier.create(booleanMono).expectNext(true).expectComplete().verify(); + } + + private static class InvalidDomain { + + private long count; + + private String location; + + InvalidDomain() { + } + + InvalidDomain(long count, String location) { + this.count = count; + this.location = location; + } + + public long getCount() { + return count; + } + + public void setCount(long 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/SecondaryTestRepositoryConfig.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/SecondaryTestRepositoryConfig.java new file mode 100644 index 000000000000..27ef9490c2f9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/SecondaryTestRepositoryConfig.java @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.repository.config.EnableReactiveCosmosRepositories; +import org.springframework.beans.factory.annotation.Qualifier; +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; + +/** + * Secondary Database Account + */ +@Configuration +@PropertySource(value = {"classpath:application.properties"}) +public class SecondaryTestRepositoryConfig { + @Value("${cosmos.secondary.uri:}") + private String cosmosDbUri; + + @Value("${cosmos.secondary.key:}") + private String cosmosDbKey; + + @Value("${cosmos.secondary.database:}") + private String database; + + @Value("${cosmos.secondary.queryMetricsEnabled}") + private boolean queryMetricsEnabled; + + @Value("${cosmos.secondary.maxDegreeOfParallelism}") + private int maxDegreeOfParallelism; + + @Value("${cosmos.secondary.maxBufferedItemCount}") + private int maxBufferedItemCount; + + @Value("${cosmos.secondary.responseContinuationTokenLimitInKb}") + private int responseContinuationTokenLimitInKb; + + @Bean + public CosmosClientBuilder secondaryCosmosClientBuilder() { + return new CosmosClientBuilder() + .key(cosmosDbKey) + .endpoint(cosmosDbUri) + .contentResponseOnWriteEnabled(true); + } + + @Bean("secondaryCosmosAsyncClient") + public CosmosAsyncClient getCosmosAsyncClient(CosmosClientBuilder secondaryCosmosClientBuilder) { + return CosmosFactory.createCosmosAsyncClient(secondaryCosmosClientBuilder); + } + + /** + * First database for this account + */ + @EnableReactiveCosmosRepositories(reactiveCosmosTemplateRef = "secondaryReactiveCosmosTemplate") + public class SecondaryDataSourceConfiguration { + @Bean + public ReactiveCosmosTemplate secondaryReactiveCosmosTemplate(@Qualifier("secondaryCosmosAsyncClient") CosmosAsyncClient client, MappingCosmosConverter mappingCosmosConverter) { + + CosmosConfig config = CosmosConfig.builder() + .enableQueryMetrics(queryMetricsEnabled) + .maxDegreeOfParallelism(maxDegreeOfParallelism) + .maxBufferedItemCount(maxBufferedItemCount) + .responseContinuationTokenLimitInKb(responseContinuationTokenLimitInKb) + .build(); + + return new ReactiveCosmosTemplate(new CosmosFactory(client, getFirstDatabase()), config, mappingCosmosConverter); + } + } + + /** + * Second database for this account + */ + @EnableReactiveCosmosRepositories(reactiveCosmosTemplateRef = "secondaryReactiveCosmosTemplate1") + public class SecondaryDataSourceConfiguration1 { + @Bean + public ReactiveCosmosTemplate secondaryReactiveCosmosTemplate1(@Qualifier("secondaryCosmosAsyncClient") CosmosAsyncClient client, MappingCosmosConverter mappingCosmosConverter) { + + CosmosConfig config = CosmosConfig.builder() + .enableQueryMetrics(queryMetricsEnabled) + .maxDegreeOfParallelism(maxDegreeOfParallelism) + .maxBufferedItemCount(maxBufferedItemCount) + .responseContinuationTokenLimitInKb(responseContinuationTokenLimitInKb) + .build(); + + return new ReactiveCosmosTemplate(new CosmosFactory(client, getSecondDatabase()), config, mappingCosmosConverter); + } + } + + private String getFirstDatabase() { + return StringUtils.hasText(this.database) ? this.database : "test_db_1"; + } + + private String getSecondDatabase() { + return "test_db_2"; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/StubAuditorProvider.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/StubAuditorProvider.java new file mode 100644 index 000000000000..88b59a3f0bae --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/StubAuditorProvider.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import org.springframework.data.domain.AuditorAware; + +import java.util.Optional; + +public class StubAuditorProvider implements AuditorAware { + + private String currentAuditor = "auditor"; + + @Override + public Optional getCurrentAuditor() { + return Optional.of(currentAuditor); + } + + public void setCurrentAuditor(String currentAuditor) { + this.currentAuditor = currentAuditor; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/StubDateTimeProvider.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/StubDateTimeProvider.java new file mode 100644 index 000000000000..2f268fc87aac --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/StubDateTimeProvider.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import org.springframework.data.auditing.DateTimeProvider; + +import java.time.OffsetDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.Optional; + +public class StubDateTimeProvider implements DateTimeProvider { + + private OffsetDateTime now = OffsetDateTime.now(); + + @Override + public Optional getNow() { + return Optional.of(now); + } + + public void setNow(OffsetDateTime now) { + this.now = now; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/TestRepositoryConfig.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/TestRepositoryConfig.java new file mode 100644 index 000000000000..2668fd2c1499 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/TestRepositoryConfig.java @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.common.ResponseDiagnosticsTestUtils; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.config.AbstractCosmosConfiguration; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.mapping.EnableCosmosAuditing; +import com.azure.spring.data.cosmos.core.mapping.event.SimpleCosmosMappingEventListener; +import com.azure.spring.data.cosmos.repository.config.EnableCosmosRepositories; +import com.azure.spring.data.cosmos.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; + +import java.util.Arrays; +import java.util.Collection; + +@Configuration +@PropertySource(value = { "classpath:application.properties" }) +@EnableCosmosRepositories +@EnableCosmosAuditing(dateTimeProviderRef = "auditingDateTimeProvider") +@EnableReactiveCosmosRepositories +public class TestRepositoryConfig extends AbstractCosmosConfiguration { + @Value("${cosmos.uri:}") + private String cosmosDbUri; + + @Value("${cosmos.key:}") + private String cosmosDbKey; + + @Value("${cosmos.database:}") + private String database; + + @Value("${cosmos.queryMetricsEnabled}") + private boolean queryMetricsEnabled; + + @Value("${cosmos.maxDegreeOfParallelism}") + private int maxDegreeOfParallelism; + + @Value("${cosmos.maxBufferedItemCount}") + private int maxBufferedItemCount; + + @Value("${cosmos.responseContinuationTokenLimitInKb}") + private int responseContinuationTokenLimitInKb; + + @Bean + public ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils() { + return new ResponseDiagnosticsTestUtils(); + } + + @Bean + public CosmosClientBuilder cosmosClientBuilder() { + return new CosmosClientBuilder() + .key(cosmosDbKey) + .endpoint(cosmosDbUri) + .contentResponseOnWriteEnabled(true); + } + + @Bean + @Override + public CosmosConfig cosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(queryMetricsEnabled) + .maxDegreeOfParallelism(maxDegreeOfParallelism) + .maxBufferedItemCount(maxBufferedItemCount) + .responseContinuationTokenLimitInKb(responseContinuationTokenLimitInKb) + .responseDiagnosticsProcessor(responseDiagnosticsTestUtils().getResponseDiagnosticsProcessor()) + .build(); + } + + @Override + protected String getDatabaseName() { + return StringUtils.hasText(this.database) ? this.database : TestConstants.DB_NAME; + } + + @Bean(name = "auditingDateTimeProvider") + public StubDateTimeProvider stubDateTimeProvider() { + return new StubDateTimeProvider(); + } + + @Bean + public StubAuditorProvider auditorProvider() { + return new StubAuditorProvider(); + } + + @Override + protected Collection getMappingBasePackages() { + final Package mappingBasePackage = getClass().getPackage(); + final String entityPackage = "com.azure.spring.data.cosmos.domain"; + return Arrays.asList(mappingBasePackage.getName(), entityPackage); + } + + @Bean + SimpleCosmosMappingEventListener simpleMappingEventListener() { + return new SimpleCosmosMappingEventListener(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/TestRepositorySpELConfig.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/TestRepositorySpELConfig.java new file mode 100644 index 000000000000..b74bf2ad4cfa --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/TestRepositorySpELConfig.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import com.azure.spring.data.cosmos.common.DynamicContainer; +import com.azure.spring.data.cosmos.common.TestConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class TestRepositorySpELConfig extends TestRepositoryConfig { + + @Bean + public DynamicContainer dynamicContainer() { + return new DynamicContainer(TestConstants.DYNAMIC_BEAN_COLLECTION_NAME); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AddressRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AddressRepositoryIT.java new file mode 100644 index 000000000000..a6c105072307 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AddressRepositoryIT.java @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.implementation.PreconditionFailedException; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.AddressRepository; +import org.assertj.core.util.Lists; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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 com.azure.spring.data.cosmos.common.TestConstants.CITY; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION1; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION2; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS2_PARTITION1; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS4_PARTITION3; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class AddressRepositoryIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + AddressRepository repository; + + @Autowired + private CosmosTemplate template; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + CosmosPatchOperations patchSetOperation = CosmosPatchOperations + .create() + .set("/street", TestConstants.NEW_STREET); + + CosmosPatchOperations patchReplaceOperation = CosmosPatchOperations + .create() + .replace("/street", TestConstants.NEW_STREET); + + CosmosPatchOperations patchRemoveOperation = CosmosPatchOperations + .create() + .remove("/street"); + + private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions(); + + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Address.class); + repository.saveAll(Lists.newArrayList(TEST_ADDRESS1_PARTITION1, TEST_ADDRESS1_PARTITION2, + TEST_ADDRESS2_PARTITION1, TEST_ADDRESS4_PARTITION3)); + } + + @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(collectionManager.getEntityInformation(Address.class).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 = TestUtils.toList(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 testFindByPartitionedCityIn() { + final String city = TEST_ADDRESS1_PARTITION1.getCity(); + final List
result = TestUtils.toList(repository.findByCityIn(Lists.newArrayList(city))); + + assertThat(result.size()).isEqualTo(2); + assertThat(result.get(0).getCity()).isEqualTo(city); + assertThat(result.get(1).getCity()).isEqualTo(city); + } + + @Test + public void testFindByPostalCodeAndCityIn() { + final String city = TEST_ADDRESS1_PARTITION1.getCity(); + final List postalCodes = Lists.newArrayList(TEST_ADDRESS1_PARTITION1.getPostalCode(), + TEST_ADDRESS2_PARTITION1.getPostalCode()); + final List
result = TestUtils.toList(repository.findByPostalCodeInAndCity(postalCodes, city)); + + assertThat(result.size()).isEqualTo(2); + assertThat(result).isEqualTo(Lists.newArrayList(TEST_ADDRESS1_PARTITION1, TEST_ADDRESS2_PARTITION1)); + } + + @Test + public void testFindByStreetOrCity() { + final String city = TEST_ADDRESS1_PARTITION1.getCity(); + final String street = TEST_ADDRESS1_PARTITION2.getStreet(); + + final List
result = TestUtils.toList(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 = + TestUtils.toList(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 = TestUtils.toList(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 = TestUtils.toList(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 = + TestUtils.toList(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()); + } + + @Test + public void testPatchEntitySet() { + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchSetOperation); + assertThat(patchedAddress.getStreet()).isEqualTo(TestConstants.NEW_STREET); + } + + @Test + public void testPatchEntityReplace() { + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchReplaceOperation); + assertThat(patchedAddress.getStreet()).isEqualTo(TestConstants.NEW_STREET); + } + + @Test + public void testPatchEntityRemove() { + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchRemoveOperation); + assertNull(patchedAddress.getStreet()); + } + @Test + public void testPatchPreConditionSuccess() { + options.setFilterPredicate("FROM address a WHERE a.city = '"+CITY+"'"); + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchSetOperation, options); + assertThat(patchedAddress.getStreet()).isEqualTo(TestConstants.NEW_STREET); + } + + @Test + public void testPatchPreConditionFail() { + try { + options.setFilterPredicate("FROM address a WHERE a.city = 'dummy'"); + Address patchedAddress = repository.save(TestConstants.POSTAL_CODE, new PartitionKey(CITY), Address.class, patchSetOperation, options); + assertThat(patchedAddress.getStreet()).isEqualTo(TestConstants.NEW_STREET); + Assert.fail(); + } catch (CosmosAccessException ex) { + assertThat(ex.getCosmosException()).isInstanceOf(PreconditionFailedException.class); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AnnotatedQueryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AnnotatedQueryIT.java new file mode 100644 index 000000000000..590f78b4c1ce --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AnnotatedQueryIT.java @@ -0,0 +1,318 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.domain.AuditableEntity; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.AddressRepository; +import com.azure.spring.data.cosmos.repository.repository.AuditableRepository; +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Before; +import org.junit.ClassRule; +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.PageRequest; +import org.springframework.data.domain.Slice; +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.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import static com.azure.spring.data.cosmos.common.PageTestUtils.validateLastPage; +import static com.azure.spring.data.cosmos.common.PageTestUtils.validateNonLastPage; +import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_1; +import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_2; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class AnnotatedQueryIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private AuditableRepository auditableRepository; + + @Autowired + private AddressRepository addressRepository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Address.class, AuditableEntity.class); + } + + @Test + public void testAnnotatedQuery() { + addressRepository.saveAll(Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS1_PARTITION2)); + + final List
result = addressRepository.annotatedFindListByCity(Address.TEST_ADDRESS1_PARTITION1.getCity()); + assertThat(result).isNotNull(); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0)).isEqualTo(Address.TEST_ADDRESS1_PARTITION1); + } + + @Test + public void testAnnotatedQueryWithReturnTypeContainingLocalDateTime() { + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + + final AuditableEntity savedEntity = auditableRepository.save(entity); + + final List result = auditableRepository.annotatedFindById(savedEntity.getId()); + assertThat(result).isNotNull(); + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getId()).isEqualTo(entity.getId()); + } + + @Test + public void testAnnotatedQueryWithPageable() { + addressRepository.saveAll(Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1)); + + final PageRequest pageRequest = CosmosPageRequest.of(0, PAGE_SIZE_1); + final Page
page = addressRepository.annotatedFindByCity(TestConstants.CITY, pageRequest); + + assertThat(page.getContent().size()).isEqualTo(PAGE_SIZE_1); + validateResultStreetMatch(page, Address.TEST_ADDRESS1_PARTITION1.getStreet()); + validateNonLastPage(page, PAGE_SIZE_1); + + final Page
nextPage = addressRepository.annotatedFindByCity(TestConstants.CITY, page.nextPageable()); + + assertThat(nextPage.getContent().size()).isEqualTo(PAGE_SIZE_1); + validateResultStreetMatch(nextPage, Address.TEST_ADDRESS2_PARTITION1.getStreet()); + validateLastPage(nextPage, PAGE_SIZE_1); + } + + private void validateResultStreetMatch(Page
page, String street) { + for (Address result : page.getContent()) { + assertThat(result.getStreet()).isEqualTo(street); + } + } + + @Test + public void testAnnotatedQueryWithSort() { + addressRepository.saveAll(Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1)); + + final List
resultsAsc = addressRepository.annotatedFindByCity(TestConstants.CITY, Sort.by(Sort.Direction.ASC, "street")); + assertAddressOrder(resultsAsc, Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1); + + final List
resultsDesc = addressRepository.annotatedFindByCity(TestConstants.CITY, Sort.by(Sort.Direction.DESC, "street")); + assertAddressOrder(resultsDesc, Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION1); + } + + @Test + public void testAnnotatedQueryWithValueAsPage() { + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1); + addressRepository.saveAll(addresses); + + final PageRequest cosmosPageRequest = CosmosPageRequest.of(0, 10); + final Page postalCodes = addressRepository.annotatedFindPostalCodeValuesByCity(TestConstants.CITY, + cosmosPageRequest); + + assertAddressPostalCodesUnordered(postalCodes.getContent(), addresses); + } + + @Test + public void testAnnotatedQueryWithValueAsPageTwoPages() { + Address testAddress = new Address(TestConstants.POSTAL_CODE_1, TestConstants.STREET_0, TestConstants.CITY); + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, + Address.TEST_ADDRESS2_PARTITION1, testAddress); + addressRepository.saveAll(addresses); + + final PageRequest cosmosPageRequest = CosmosPageRequest.of(0, 2, Sort.by(Sort.Direction.ASC, "postalCode")); + final Page postalCodes = addressRepository.annotatedFindPostalCodeValuesByCity(TestConstants.CITY, + cosmosPageRequest); + assertAddressPostalCodesUnordered(postalCodes.getContent(), + Arrays.asList(Address.TEST_ADDRESS2_PARTITION1, testAddress)); + + final PageRequest cosmosPageRequest2 = cosmosPageRequest.next(); + final Page postalCodes2 = addressRepository.annotatedFindPostalCodeValuesByCity(TestConstants.CITY, + cosmosPageRequest2); + assertAddressPostalCodesUnordered(postalCodes2.getContent(), Arrays.asList(Address.TEST_ADDRESS1_PARTITION1)); + } + + @Test + public void testAnnotatedQueryWithValueAsPageFromPageOneToThree() { + Address testAddress = new Address(TestConstants.POSTAL_CODE_1, TestConstants.STREET_0, TestConstants.CITY); + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, + Address.TEST_ADDRESS2_PARTITION1, testAddress); + addressRepository.saveAll(addresses); + + final PageRequest cosmosPageRequest = CosmosPageRequest.of(0, 1, Sort.by(Sort.Direction.ASC, "postalCode")); + final Page postalCodes = addressRepository.annotatedFindPostalCodeValuesByCity(TestConstants.CITY, + cosmosPageRequest); + assertAddressPostalCodesUnordered(postalCodes.getContent(), Arrays.asList(Address.TEST_ADDRESS2_PARTITION1)); + + final PageRequest cosmosPageRequest2 = CosmosPageRequest.of(2, 1, Sort.by(Sort.Direction.ASC, "postalCode")); + final Page postalCodes2 = addressRepository.annotatedFindPostalCodeValuesByCity(TestConstants.CITY, + cosmosPageRequest2); + assertAddressPostalCodesUnordered(postalCodes2.getContent(), Arrays.asList(Address.TEST_ADDRESS1_PARTITION1)); + } + + @Test + public void testAnnotatedQueryWithValueAsPageMultiplePageSizes() { + /* + * Will have 6 total results in the following order: + * 11111, 22222, 333333, 444444, 55555, 98052 + */ + Address testAddress1 = new Address("22222", TestConstants.STREET_0, TestConstants.CITY); + Address testAddress2 = new Address("33333", TestConstants.STREET_0, TestConstants.CITY); + Address testAddress3 = new Address("44444", TestConstants.STREET_0, TestConstants.CITY); + Address testAddress4 = new Address("55555", TestConstants.STREET_0, TestConstants.CITY); + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, + Address.TEST_ADDRESS2_PARTITION1, testAddress1, testAddress2, testAddress3, testAddress4); + addressRepository.saveAll(addresses); + + final PageRequest cosmosPageRequest = CosmosPageRequest.of(0, 1, Sort.by(Sort.Direction.ASC, "postalCode")); + final Page postalCodes = addressRepository.annotatedFindPostalCodeValuesByCity(TestConstants.CITY, + cosmosPageRequest); + assertAddressPostalCodesUnordered(postalCodes.getContent(), Arrays.asList(Address.TEST_ADDRESS2_PARTITION1)); + + final PageRequest cosmosPageRequest2 = CosmosPageRequest.of(1, 3, Sort.by(Sort.Direction.ASC, "postalCode")); + final Page postalCodes2 = addressRepository.annotatedFindPostalCodeValuesByCity(TestConstants.CITY, + cosmosPageRequest2); + assertAddressPostalCodesUnordered(postalCodes2.getContent(), Arrays.asList(testAddress3, testAddress4, + Address.TEST_ADDRESS1_PARTITION1)); + + final PageRequest cosmosPageRequest3 = CosmosPageRequest.of(2, 2, Sort.by(Sort.Direction.ASC, "postalCode")); + final Page postalCodes3 = addressRepository.annotatedFindPostalCodeValuesByCity(TestConstants.CITY, + cosmosPageRequest3); + assertAddressPostalCodesUnordered(postalCodes3.getContent(), Arrays.asList(testAddress4, + Address.TEST_ADDRESS1_PARTITION1)); + } + + @Test + public void testAnnotatedQueryWithValueAsList() { + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1); + addressRepository.saveAll(addresses); + + final List postalCodes = addressRepository.annotatedFindPostalCodeValuesByCity(TestConstants.CITY); + + assertAddressPostalCodesUnordered(postalCodes, addresses); + } + + @Test + public void testAnnotatedQueryWithJsonNodeAsPage() { + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1); + addressRepository.saveAll(addresses); + + final PageRequest cosmosPageRequest = CosmosPageRequest.of(0, 10); + final Page postalCodes = addressRepository.annotatedFindPostalCodesByCity(TestConstants.CITY, + cosmosPageRequest); + final List actualPostalCodes = postalCodes.getContent() + .stream() + .map(jsonNode -> jsonNode.get("postalCode").asText()) + .collect(Collectors.toList()); + assertAddressPostalCodesUnordered(actualPostalCodes, addresses); + } + + @Test + public void testAnnotatedQueryWithJsonNodeAsSlice() { + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1); + addressRepository.saveAll(addresses); + + final PageRequest cosmosPageRequest = CosmosPageRequest.of(0, 10); + final Slice postalCodes = addressRepository.annotatedFindPostalCodesByCityAsSlice(TestConstants.CITY, + cosmosPageRequest); + final List actualPostalCodes = postalCodes.getContent() + .stream() + .map(jsonNode -> jsonNode.get("postalCode").asText()) + .collect(Collectors.toList()); + assertAddressPostalCodesUnordered(actualPostalCodes, addresses); + } + + private void assertAddressPostalCodesUnordered(List postalCodes, List
expectedResults) { + List expectedPostalCodes = expectedResults.stream() + .map(Address::getPostalCode) + .collect(Collectors.toList()); + + assertThat(postalCodes).hasSize(expectedPostalCodes.size()).hasSameElementsAs(expectedPostalCodes); + } + + private void assertAddressOrder(List
actualResults, Address ... expectedResults) { + assertThat(actualResults.size()).isEqualTo(expectedResults.length); + for (int i = 0; i < expectedResults.length; i++) { + assertThat(expectedResults[i]).isEqualTo(actualResults.get(i)); + } + } + + @Test + public void testAnnotatedQueryWithPageableSort() { + addressRepository.saveAll(Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1)); + + final PageRequest ascPageRequest = CosmosPageRequest.of(0, PAGE_SIZE_2, Sort.by(Sort.Direction.ASC, "street")); + final Page
ascPage = addressRepository.annotatedFindByCity(TestConstants.CITY, ascPageRequest); + assertAddressOrder(ascPage.getContent(), Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1); + + final PageRequest descPageRequest = CosmosPageRequest.of(0, PAGE_SIZE_2, Sort.by(Sort.Direction.DESC, "street")); + final Page
descPage = addressRepository.annotatedFindByCity(TestConstants.CITY, descPageRequest); + assertAddressOrder(descPage.getContent(), Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION1); + } + + @Test + public void testAnnotatedQueryWithMultipleCitiesAndSort() { + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION2); + addressRepository.saveAll(addresses); + + List cities = new ArrayList<>(); + cities.add(TestConstants.CITY); + final List
resultsAsc = addressRepository.annotatedFindByCityIn(cities, Sort.by(Sort.Direction.ASC, "postalCode")); + assertAddressOrder(resultsAsc, Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION1); + + List cities2 = new ArrayList<>(); + cities2.add(TestConstants.CITY); + cities2.add(TestConstants.CITY_0); + final List
resultsAsc2 = addressRepository.annotatedFindByCityIn(cities2, Sort.by(Sort.Direction.ASC, "postalCode")); + assertAddressOrder(resultsAsc2, Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION2, Address.TEST_ADDRESS1_PARTITION1); + } + + @Test + public void testAnnotatedQueryWithArrayContains() { + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION2); + addressRepository.saveAll(addresses); + + List cities = new ArrayList<>(); + cities.add(TestConstants.CITY); + final List
resultsAsc = addressRepository.annotatedFindByCities(cities); + assertAddressOrder(resultsAsc, Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1); + + List cities2 = new ArrayList<>(); + cities2.add(TestConstants.CITY); + cities2.add(TestConstants.CITY_0); + final List
resultsAsc2 = addressRepository.annotatedFindByCities(cities2); + assertAddressOrder(resultsAsc2, Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION2); + + } + + @Test + public void testAnnotatedQueryWithArrayContainsAndSort() { + final List
addresses = Arrays.asList(Address.TEST_ADDRESS1_PARTITION1, Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION2); + addressRepository.saveAll(addresses); + + List cities = new ArrayList<>(); + cities.add(TestConstants.CITY); + final List
resultsAsc = addressRepository.annotatedFindByCitiesWithSort(cities, Sort.by(Sort.Direction.ASC, "postalCode")); + assertAddressOrder(resultsAsc, Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION1); + + List cities2 = new ArrayList<>(); + cities2.add(TestConstants.CITY); + cities2.add(TestConstants.CITY_0); + final List
resultsAsc2 = addressRepository.annotatedFindByCitiesWithSort(cities2, Sort.by(Sort.Direction.ASC, "postalCode")); + assertAddressOrder(resultsAsc2, Address.TEST_ADDRESS2_PARTITION1, Address.TEST_ADDRESS1_PARTITION2, Address.TEST_ADDRESS1_PARTITION1); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AuditableIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AuditableIT.java new file mode 100644 index 000000000000..fb6da5d3ecae --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AuditableIT.java @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.generator.FindQuerySpecGenerator; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.domain.AuditableEntity; +import com.azure.spring.data.cosmos.domain.AuditableIdGeneratedEntity; +import com.azure.spring.data.cosmos.repository.StubAuditorProvider; +import com.azure.spring.data.cosmos.repository.StubDateTimeProvider; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.AuditableIdGeneratedRepository; +import com.azure.spring.data.cosmos.repository.repository.AuditableRepository; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class AuditableIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate cosmosTemplate; + @Autowired + private AuditableRepository auditableRepository; + @Autowired + private AuditableIdGeneratedRepository auditableIdGeneratedRepository; + @Autowired + private StubDateTimeProvider stubDateTimeProvider; + @Autowired + private StubAuditorProvider stubAuditorProvider; + + @Before + public void setup() { + collectionManager.ensureContainersCreatedAndEmpty(cosmosTemplate, AuditableEntity.class, AuditableIdGeneratedEntity.class); + } + + @Test + public void testInsertShouldSetAuditableEntries() { + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + final OffsetDateTime now = OffsetDateTime.now(ZoneId.of("UTC")).truncatedTo(ChronoUnit.MICROS); + + stubDateTimeProvider.setNow(now); + stubAuditorProvider.setCurrentAuditor("created-by"); + final AuditableEntity savedEntity = auditableRepository.save(entity); + + assertThat(savedEntity.getCreatedBy()).isEqualTo("created-by"); + assertThat(savedEntity.getCreatedDate()).isEqualTo(now); + assertThat(savedEntity.getLastModifiedBy()).isEqualTo("created-by"); + assertThat(savedEntity.getLastModifiedByDate()).isEqualTo(now); + } + + @Test + public void testUpdateShouldNotOverwriteCreatedEntries() { + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + final OffsetDateTime createdOn = OffsetDateTime.now(ZoneId.of("UTC")).truncatedTo(ChronoUnit.MICROS); + + stubDateTimeProvider.setNow(createdOn); + stubAuditorProvider.setCurrentAuditor("created-by"); + final AuditableEntity savedEntity = auditableRepository.save(entity); + + final OffsetDateTime modifiedOn = createdOn.plusMinutes(1); + stubDateTimeProvider.setNow(modifiedOn); + stubAuditorProvider.setCurrentAuditor("modified-by"); + final AuditableEntity modifiedEntity = auditableRepository.save(savedEntity); + + assertThat(modifiedEntity.getCreatedBy()).isEqualTo("created-by"); + assertThat(modifiedEntity.getCreatedDate()).isEqualTo(createdOn); + assertThat(modifiedEntity.getLastModifiedBy()).isEqualTo("modified-by"); + assertThat(modifiedEntity.getLastModifiedByDate()).isEqualTo(modifiedOn); + } + + @Test + public void testInsertShouldSetAuditableEntriesIfIdAutoGenerated() { + final AuditableIdGeneratedEntity entity = new AuditableIdGeneratedEntity(); + final OffsetDateTime now = OffsetDateTime.now(ZoneId.of("UTC")).truncatedTo(ChronoUnit.MICROS); + + stubDateTimeProvider.setNow(now); + stubAuditorProvider.setCurrentAuditor("created-by"); + final AuditableIdGeneratedEntity savedEntity = auditableIdGeneratedRepository.save(entity); + + assertThat(savedEntity.getCreatedBy()).isEqualTo("created-by"); + assertThat(savedEntity.getCreatedDate()).isEqualTo(now); + assertThat(savedEntity.getLastModifiedBy()).isEqualTo("created-by"); + assertThat(savedEntity.getLastModifiedByDate()).isEqualTo(now); + } + + @Test + public void testRunQueryWithReturnTypeContainingLocalDateTime() { + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + + auditableRepository.save(entity); + + Criteria equals = Criteria.getInstance(CriteriaType.IS_EQUAL, "id", Collections.singletonList(entity.getId()), Part.IgnoreCaseType.NEVER); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(new CosmosQuery(equals)); + List results = TestUtils.toList(cosmosTemplate.runQuery(sqlQuerySpec, AuditableEntity.class, AuditableEntity.class)); + assertEquals(results.size(), 1); + AuditableEntity foundEntity = results.get(0); + assertEquals(entity.getId(), foundEntity.getId()); + assertNotNull(foundEntity.getCreatedDate()); + assertNotNull(foundEntity.getLastModifiedByDate()); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CompositeIndexIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CompositeIndexIT.java new file mode 100644 index 000000000000..e2c32edc9444 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CompositeIndexIT.java @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.implementation.ImplementationBridgeHelpers; +import com.azure.cosmos.models.CompositePath; +import com.azure.cosmos.models.CompositePathSortOrder; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.IndexingPolicy; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.domain.CompositeIndexEntity; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.azure.spring.data.cosmos.repository.support.SimpleCosmosRepository; +import com.azure.spring.data.cosmos.repository.support.SimpleReactiveCosmosRepository; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +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 static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CompositeIndexIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + CosmosTemplate template; + + @Autowired + ReactiveCosmosTemplate reactiveTemplate; + + CosmosEntityInformation information = new CosmosEntityInformation<>(CompositeIndexEntity.class); + + @Before + public void setup() { + collectionManager.ensureContainersCreatedAndEmpty(template, CompositeIndexEntity.class); + } + + @Test + public void canSetCompositeIndex() { + collectionManager.deleteContainer(information); + new SimpleCosmosRepository<>(information, template); + CosmosContainerProperties properties = template.getContainerProperties(information.getContainerName()); + List> indexes = properties.getIndexingPolicy().getCompositeIndexes(); + + assertThat(indexes.get(0).get(0).getPath()).isEqualTo("/fieldOne"); + assertThat(indexes.get(0).get(0).getOrder()).isEqualTo(CompositePathSortOrder.ASCENDING); + assertThat(indexes.get(0).get(1).getPath()).isEqualTo("/fieldTwo"); + assertThat(indexes.get(0).get(1).getOrder()).isEqualTo(CompositePathSortOrder.ASCENDING); + + assertThat(indexes.get(1).get(0).getPath()).isEqualTo("/fieldThree"); + assertThat(indexes.get(1).get(0).getOrder()).isEqualTo(CompositePathSortOrder.DESCENDING); + assertThat(indexes.get(1).get(1).getPath()).isEqualTo("/fieldFour"); + assertThat(indexes.get(1).get(1).getOrder()).isEqualTo(CompositePathSortOrder.DESCENDING); + } + + @Test + public void canSetCompositeIndexReactive() { + collectionManager.deleteContainer(information); + new SimpleReactiveCosmosRepository<>(information, reactiveTemplate); + CosmosContainerProperties properties = reactiveTemplate.getContainerProperties(information.getContainerName()).block(); + List> indexes = properties.getIndexingPolicy().getCompositeIndexes(); + + assertThat(indexes.get(0).get(0).getPath()).isEqualTo("/fieldOne"); + assertThat(indexes.get(0).get(0).getOrder()).isEqualTo(CompositePathSortOrder.ASCENDING); + assertThat(indexes.get(0).get(1).getPath()).isEqualTo("/fieldTwo"); + assertThat(indexes.get(0).get(1).getOrder()).isEqualTo(CompositePathSortOrder.ASCENDING); + + assertThat(indexes.get(1).get(0).getPath()).isEqualTo("/fieldThree"); + assertThat(indexes.get(1).get(0).getOrder()).isEqualTo(CompositePathSortOrder.DESCENDING); + assertThat(indexes.get(1).get(1).getPath()).isEqualTo("/fieldFour"); + assertThat(indexes.get(1).get(1).getOrder()).isEqualTo(CompositePathSortOrder.DESCENDING); + } + + + @Test + public void canNotUpdateCompositeIndex() { + // initialize policy on entity + new SimpleCosmosRepository<>(information, template); + + // set new index policy + IndexingPolicy newIndexPolicy = new IndexingPolicy(); + List> newCompositeIndex = new ArrayList<>(); + List innerList = new ArrayList<>(); + innerList.add(new CompositePath().setPath("/fieldOne")); + innerList.add(new CompositePath().setPath("/fieldFour")); + newCompositeIndex.add(innerList); + newIndexPolicy.setCompositeIndexes(newCompositeIndex); + + // apply new index policy + CosmosEntityInformation spyEntityInformation = Mockito.spy(information); + Mockito.doReturn(newIndexPolicy).when(spyEntityInformation).getIndexingPolicy(); + new SimpleCosmosRepository<>(spyEntityInformation, template); + + // retrieve new policy + CosmosContainerProperties properties = template.getContainerProperties(information.getContainerName()); + List> indexes = properties.getIndexingPolicy().getCompositeIndexes(); + + // assert + assertThat(indexes.size()).isEqualTo(2); + assertThat(indexes.get(0).get(0).getPath()).isEqualTo("/fieldOne"); + assertThat(indexes.get(0).get(0).getOrder()).isEqualTo(CompositePathSortOrder.ASCENDING); + assertThat(indexes.get(0).get(1).getPath()).isEqualTo("/fieldTwo"); + assertThat(indexes.get(0).get(1).getOrder()).isEqualTo(CompositePathSortOrder.ASCENDING); + assertThat(indexes.get(1).get(0).getPath()).isEqualTo("/fieldThree"); + assertThat(indexes.get(1).get(0).getOrder()).isEqualTo(CompositePathSortOrder.DESCENDING); + assertThat(indexes.get(1).get(1).getPath()).isEqualTo("/fieldFour"); + assertThat(indexes.get(1).get(1).getOrder()).isEqualTo(CompositePathSortOrder.DESCENDING); + } + + @Test + public void canNotUpdateCompositeIndexReactive() { + // initialize policy on entity + new SimpleReactiveCosmosRepository<>(information, reactiveTemplate); + + // set new index policy + IndexingPolicy newIndexPolicy = new IndexingPolicy(); + List> newCompositeIndex = new ArrayList<>(); + List innerList = new ArrayList<>(); + innerList.add(new CompositePath().setPath("/fieldOne")); + innerList.add(new CompositePath().setPath("/fieldFour")); + newCompositeIndex.add(innerList); + newIndexPolicy.setCompositeIndexes(newCompositeIndex); + + // apply new index policy + CosmosEntityInformation spyEntityInformation = Mockito.spy(information); + Mockito.doReturn(newIndexPolicy).when(spyEntityInformation).getIndexingPolicy(); + new SimpleReactiveCosmosRepository<>(spyEntityInformation, reactiveTemplate); + + // retrieve new policy + CosmosContainerProperties properties = reactiveTemplate.getContainerProperties(information.getContainerName()).block(); + List> indexes = properties.getIndexingPolicy().getCompositeIndexes(); + + // assert + assertThat(indexes.size()).isEqualTo(2); + assertThat(indexes.get(0).get(0).getPath()).isEqualTo("/fieldOne"); + assertThat(indexes.get(0).get(0).getOrder()).isEqualTo(CompositePathSortOrder.ASCENDING); + assertThat(indexes.get(0).get(1).getPath()).isEqualTo("/fieldTwo"); + assertThat(indexes.get(0).get(1).getOrder()).isEqualTo(CompositePathSortOrder.ASCENDING); + assertThat(indexes.get(1).get(0).getPath()).isEqualTo("/fieldThree"); + assertThat(indexes.get(1).get(0).getOrder()).isEqualTo(CompositePathSortOrder.DESCENDING); + assertThat(indexes.get(1).get(1).getPath()).isEqualTo("/fieldFour"); + assertThat(indexes.get(1).get(1).getOrder()).isEqualTo(CompositePathSortOrder.DESCENDING); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ContactRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ContactRepositoryIT.java new file mode 100644 index 000000000000..2f7c30df018f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ContactRepositoryIT.java @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Contact; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ContactRepository; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.assertj.core.util.Lists; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.Arrays; +import java.util.Iterator; +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 Integer INT_VALUE_1 = 25; + private static final Integer INT_VALUE_2 = 32; + private static final Integer INT_VALUE_3 = 43; + private static final Contact TEST_CONTACT1 = new Contact("testId", "faketitle", INT_VALUE_1, true); + private static final Contact TEST_CONTACT2 = new Contact("testId2", "faketitle2", INT_VALUE_2, false); + private static final Contact TEST_CONTACT3 = new Contact("testId3", "faketitle3", INT_VALUE_1, false); + private static final Contact TEST_CONTACT4 = new Contact("testId4", "faketitle4", INT_VALUE_3, true); + private static final Contact TEST_CONTACT5 = new Contact("testId5", "faketitle3", INT_VALUE_3, true); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + ContactRepository repository; + + @Autowired + private CosmosTemplate template; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Contact.class); + repository.save(TEST_CONTACT1); + repository.save(TEST_CONTACT2); + repository.save(TEST_CONTACT3); + repository.save(TEST_CONTACT4); + repository.save(TEST_CONTACT5); + } + + @Test + public void testFindAll() { + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(5); + Assert.assertEquals(Arrays.asList(TEST_CONTACT1, TEST_CONTACT2, TEST_CONTACT3, TEST_CONTACT4, + TEST_CONTACT5), result); + + final Contact contact = repository.findById(TEST_CONTACT1.getLogicId()).get(); + + assertThat(contact.getLogicId()).isEqualTo(TEST_CONTACT1.getLogicId()); + assertThat(contact.getTitle()).isEqualTo(TEST_CONTACT1.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(6); + + long count = repository.count(); + assertThat(count).isEqualTo(6); + + repository.deleteById(contact2.getLogicId()); + + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(5); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT1.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT1.getTitle()); + + count = repository.count(); + assertThat(count).isEqualTo(5); + } + + @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(6); + + repository.delete(contact2); + + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(5); + Assert.assertEquals(Arrays.asList(TEST_CONTACT1, TEST_CONTACT2, TEST_CONTACT3, TEST_CONTACT4, + TEST_CONTACT5), result); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT1.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT1.getTitle()); + } + + @Test + public void testUpdateEntity() { + final Contact updatedContact = new Contact(TEST_CONTACT1.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_CONTACT1.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 = TestUtils.toList(repository.findByTitle(TEST_CONTACT1.getTitle())); + + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT1.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT1.getTitle()); + + } + + @Test + public void testFindById() { + final Optional optional = repository.findById(TEST_CONTACT1.getLogicId()); + + Assert.assertTrue(optional.isPresent()); + Assert.assertEquals(TEST_CONTACT1, optional.get()); + Assert.assertFalse(repository.findById("").isPresent()); + } + + @Test + public void testFindByIdNotFound() { + final Optional optional = repository.findById("unknown-id"); + + Assert.assertFalse(optional.isPresent()); + } + + @Test + public void testShouldFindSingleEntity() { + final Contact contact = repository.findOneByTitle(TEST_CONTACT1.getTitle()); + + Assert.assertEquals(TEST_CONTACT1, contact); + } + + @Test + public void testShouldFindSingleOptionalEntity() { + final Optional contact = repository.findOptionallyByTitle(TEST_CONTACT1.getTitle()); + Assert.assertTrue(contact.isPresent()); + Assert.assertEquals(TEST_CONTACT1, contact.get()); + + Assert.assertFalse(repository.findOptionallyByTitle("not here").isPresent()); + } + + @Test(expected = CosmosAccessException.class) + public void testShouldFailIfMultipleResultsReturned() { + repository.save(new Contact("testId2", TEST_CONTACT1.getTitle())); + + repository.findOneByTitle(TEST_CONTACT1.getTitle()); + } + + @Test + public void testShouldAllowListAndIterableResponses() { + final List contactList = TestUtils.toList(repository.findByTitle(TEST_CONTACT1.getTitle())); + Assert.assertEquals(TEST_CONTACT1, contactList.get(0)); + Assert.assertEquals(1, contactList.size()); + + final Iterator contactIterator = repository.findByLogicId(TEST_CONTACT1.getLogicId()).iterator(); + Assert.assertTrue(contactIterator.hasNext()); + Assert.assertEquals(TEST_CONTACT1, contactIterator.next()); + Assert.assertFalse(contactIterator.hasNext()); + } + + @Test + public void testAnnotatedQueries() { + List valueContacts = repository.getContactsByTitleAndValue(43, TEST_CONTACT5.getTitle()); + Assert.assertEquals(1, valueContacts.size()); + Assert.assertEquals(TEST_CONTACT5, valueContacts.get(0)); + + List contactsWithOffset = repository.getContactsWithOffsetLimit(1, 2); + Assert.assertEquals(2, contactsWithOffset.size()); + Assert.assertEquals(TEST_CONTACT2, contactsWithOffset.get(0)); + Assert.assertEquals(TEST_CONTACT3, contactsWithOffset.get(1)); + + List groupByContacts = repository.selectGroupBy(); + Assert.assertEquals(3, groupByContacts.size()); + } + + @Test + public void testAnnotatedQueriesDistinctIntValue() { + List valueContacts = repository.findDistinctIntValueValues(); + assertThat(valueContacts).isEqualTo(Arrays.asList(INT_VALUE_1, INT_VALUE_2, INT_VALUE_3)); + } + + @Test + public void testAnnotatedQueriesDistinctStatus() { + List statusContacts = repository.findDistinctStatusValues(); + + List expectedResults = Arrays.asList(Boolean.TRUE, Boolean.FALSE); + assertThat(statusContacts).hasSize(expectedResults.size()).hasSameElementsAs(expectedResults); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CosmosAnnotationIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CosmosAnnotationIT.java new file mode 100644 index 000000000000..a620efe1c5e1 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CosmosAnnotationIT.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.models.ExcludedPath; +import com.azure.cosmos.models.IncludedPath; +import com.azure.cosmos.models.IndexingPolicy; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Role; +import com.azure.spring.data.cosmos.domain.TimeToLiveSample; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveRoleRepository; +import org.junit.Before; +import org.junit.ClassRule; +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 org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CosmosAnnotationIT { + + private static final Role TEST_ROLE_1 = new Role(TestConstants.ID_1, true, TestConstants.LEVEL, + TestConstants.ROLE_NAME); + private static final Role TEST_ROLE_2 = new Role(TestConstants.ID_2, false, TestConstants.LEVEL, + TestConstants.ROLE_NAME); + private static final Role TEST_ROLE_3 = new Role(TestConstants.ID_3, true, TestConstants.LEVEL, + TestConstants.ROLE_NAME); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate cosmosTemplate; + @Autowired + private ReactiveRoleRepository repository; + + @Before + public void setUp() throws ClassNotFoundException { + collectionManager.ensureContainersCreatedAndEmpty(cosmosTemplate, Role.class, TimeToLiveSample.class); + repository.saveAll(Arrays.asList(TEST_ROLE_1, TEST_ROLE_2, TEST_ROLE_3)).collectList().block(); + } + + @Test + public void testFindAll() { + Flux findAll = repository.findAll(); + StepVerifier.create(findAll).expectNextCount(3).verifyComplete(); + } + + @Test + public void testFindByPartitionKey() { + Flux findAll = repository.findAll(new PartitionKey(false)); + StepVerifier.create(findAll).expectNext(TEST_ROLE_2).verifyComplete(); + } + + @Test + public void testFindByIdAndPartitionKey() { + Mono findAll = repository.findById(TEST_ROLE_2.getId(), new PartitionKey(false)); + StepVerifier.create(findAll).expectNext(TEST_ROLE_2).verifyComplete(); + } + + @Test + public void testFindByIdAndPartitionKeyNotFound() { + Mono findByIdNotFound = repository.findById(TEST_ROLE_2.getId(), new PartitionKey(true)); + StepVerifier.create(findByIdNotFound).expectNextCount(0).verifyComplete(); + } + + @Test + public void testSave() { + final Role testRole = new Role(TestConstants.ID_4, true, TestConstants.LEVEL, + TestConstants.ROLE_NAME); + Mono save = repository.save(testRole); + StepVerifier.create(save).expectNext(testRole).verifyComplete(); + } + + @Test + public void testDelete() { + Mono delete = repository.delete(TEST_ROLE_2); + StepVerifier.create(delete).verifyComplete(); + + Flux findAll = repository.findAll(new PartitionKey(true)); + StepVerifier.create(findAll).expectNextCount(2).verifyComplete(); + } + + @Test + public void testDeleteByIdAndPartitionKey() { + Mono delete = repository.deleteById(TEST_ROLE_1.getId(), new PartitionKey(true)); + StepVerifier.create(delete).verifyComplete(); + + Flux findAll = repository.findAll(); + StepVerifier.create(findAll).expectNextCount(2).verifyComplete(); + } + + @Test + public void testTimeToLiveAnnotation() { + Integer timeToLive = collectionManager.getEntityInformation(Role.class).getTimeToLive(); + assertThat(timeToLive).isEqualTo(collectionManager.getContainerProperties(Role.class).getDefaultTimeToLiveInSeconds()); + + timeToLive = collectionManager.getEntityInformation(TimeToLiveSample.class).getTimeToLive(); + assertThat(timeToLive).isEqualTo(collectionManager.getContainerProperties(TimeToLiveSample.class).getDefaultTimeToLiveInSeconds()); + } + + @Test + public void testIndexingPolicyAnnotation() { + final IndexingPolicy policy = collectionManager.getContainerProperties(Role.class).getIndexingPolicy(); + + Assert.isTrue(policy.getIndexingMode() == TestConstants.INDEXING_POLICY_MODE, + "unmatched collection policy indexing mode of class Role"); + Assert.isTrue(policy.isAutomatic() == TestConstants.INDEXING_POLICY_AUTOMATIC, + "unmatched collection policy automatic of class Role"); + + TestUtils.testIndexingPolicyPathsEquals(policy.getIncludedPaths() + .stream() + .map(IncludedPath::getPath) + .collect(Collectors.toList()), + TestConstants.INCLUDED_PATHS); + TestUtils.testIndexingPolicyPathsEquals(policy.getExcludedPaths() + .stream() + .map(ExcludedPath::getPath) + .collect(Collectors.toList()), + TestConstants.EXCLUDED_PATHS); + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CountIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CountIT.java new file mode 100644 index 000000000000..5f8211521f1e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CountIT.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Contact; +import com.azure.spring.data.cosmos.domain.Course; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ContactRepository; +import com.azure.spring.data.cosmos.repository.repository.ReactiveCourseRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class CountIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private ContactRepository repository; + @Autowired + private ReactiveCourseRepository reactiveRepository; + @Autowired + private CosmosTemplate template; + + @Before + public void setUp() throws ClassNotFoundException { + collectionManager.ensureContainersCreatedAndEmpty(template, Contact.class, Course.class); + } + + @Test + public void testCountByField() { + Contact contact1 = new Contact("1", "title", 25, true); + Contact contact2 = new Contact("2", "title", 30, true); + Contact contact3 = new Contact("3", "title", 30, true); + Contact contact4 = new Contact("4", "other", 30, true); + repository.saveAll(Arrays.asList(contact1, contact2, contact3, contact4)); + + Assert.assertEquals(3, repository.countByTitle("title")); + Assert.assertEquals(1, repository.countByTitle("other")); + + Assert.assertEquals(Long.valueOf(1), repository.countByTitleAndIntValue("title", 25)); + Assert.assertEquals(Long.valueOf(2), repository.countByTitleAndIntValue("title", 30)); + } + + @Test + public void testCountByQuery() { + Contact contact1 = new Contact("1", "same"); + Contact contact2 = new Contact("2", "same"); + Contact contact3 = new Contact("3", "different"); + repository.saveAll(Arrays.asList(contact1, contact2, contact3)); + + Assert.assertEquals(2, repository.countByQueryWithPrimitive("same")); + Assert.assertEquals(1, repository.countByQueryWithPrimitive("different")); + + Assert.assertEquals(Long.valueOf(2), repository.countByQueryWithNonPrimitive("same")); + Assert.assertEquals(Long.valueOf(1), repository.countByQueryWithNonPrimitive("different")); + } + + @Test + public void testReactiveCountByField() { + Course course1 = new Course("1", "course", "department"); + Course course2 = new Course("2", "course", "department"); + Course course3 = new Course("3", "course2", "department"); + reactiveRepository.saveAll(Arrays.asList(course1, course2, course3)).blockLast(); + + Assert.assertEquals(Long.valueOf(2), reactiveRepository.countByName("course").block()); + Assert.assertEquals(Long.valueOf(1), reactiveRepository.countByName("course2").block()); + } + + @Test + public void testReactiveCountByQuery() { + Course course1 = new Course("1", "course", "department"); + Course course2 = new Course("2", "course", "department"); + Course course3 = new Course("3", "course2", "department"); + reactiveRepository.saveAll(Arrays.asList(course1, course2, course3)).blockLast(); + + Assert.assertEquals(Long.valueOf(2), reactiveRepository.countByQuery("course").block()); + Assert.assertEquals(Long.valueOf(1), reactiveRepository.countByQuery("course2").block()); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CustomerRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CustomerRepositoryIT.java new file mode 100644 index 000000000000..3128d7965125 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/CustomerRepositoryIT.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Customer; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.CustomerRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.ArrayList; +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); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CustomerRepository repository; + + @Autowired + private CosmosTemplate template; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Customer.class); + this.repository.saveAll(Arrays.asList(CUSTOMER_0, CUSTOMER_1, CUSTOMER_2)); + } + + 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); + Iterable customers = this.repository.findByUser_Name(USER_NAME_0); + List results = new ArrayList<>(); + customers.forEach(results::add); + + assertCustomerListEquals(references, results); + + customers = this.repository.findByUser_Name(FAKE_USER_NAME); + + Assert.assertFalse(customers.iterator().hasNext()); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/EtagIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/EtagIT.java new file mode 100644 index 000000000000..2c218500ad37 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/EtagIT.java @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.PersonWithEtag; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.PersonWithEtagRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.UUID; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static com.azure.spring.data.cosmos.common.TestConstants.ADDRESSES; +import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class EtagIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + CosmosTemplate template; + @Autowired + PersonWithEtagRepository personWithEtagRepository; + + @Before + public void setup() { + collectionManager.ensureContainersCreatedAndEmpty(template, PersonWithEtag.class); + } + + private static PersonWithEtag createPersonWithEtag() { + return new PersonWithEtag(UUID.randomUUID().toString(), FIRST_NAME, LAST_NAME, HOBBIES, ADDRESSES); + } + + @Test + public void testCrudOperationsShouldApplyEtag() { + final PersonWithEtag insertedPersonWithEtag = personWithEtagRepository.save(createPersonWithEtag()); + Assert.assertNotNull(insertedPersonWithEtag.getEtag()); + + insertedPersonWithEtag.setFirstName(LAST_NAME); + final PersonWithEtag updatedPersonWithEtag = personWithEtagRepository.save(insertedPersonWithEtag); + Assert.assertNotNull(updatedPersonWithEtag.getEtag()); + Assert.assertNotEquals(insertedPersonWithEtag.getEtag(), updatedPersonWithEtag.getEtag()); + + final Optional foundPersonWithEtag = personWithEtagRepository.findById(insertedPersonWithEtag.getId()); + Assert.assertTrue(foundPersonWithEtag.isPresent()); + Assert.assertNotNull(foundPersonWithEtag.get().getEtag()); + Assert.assertEquals(updatedPersonWithEtag.getEtag(), foundPersonWithEtag.get().getEtag()); + } + + @Test + public void testCrudListOperationsShouldApplyEtag() { + final List people = new ArrayList<>(); + people.add(createPersonWithEtag()); + people.add(createPersonWithEtag()); + + final List insertedPeople = toList(personWithEtagRepository.saveAll(people)); + insertedPeople.forEach(person -> Assert.assertNotNull(person.getEtag())); + + insertedPeople.forEach(person -> person.setFirstName(LAST_NAME)); + final List updatedPeople = toList(personWithEtagRepository.saveAll(insertedPeople)); + for (int i = 0; i < updatedPeople.size(); i++) { + PersonWithEtag insertedPersonWithEtag = insertedPeople.get(i); + PersonWithEtag updatedPersonWithEtag = updatedPeople.get(i); + Assert.assertEquals(insertedPersonWithEtag.getId(), updatedPersonWithEtag.getId()); + Assert.assertNotNull(updatedPersonWithEtag.getEtag()); + Assert.assertNotEquals(insertedPersonWithEtag.getEtag(), updatedPersonWithEtag.getEtag()); + } + + final List peopleIds = updatedPeople.stream() + .map(PersonWithEtag::getId) + .collect(Collectors.toList()); + final List foundPeople = toList(personWithEtagRepository.findAllById(peopleIds)); + for (int i = 0; i < foundPeople.size(); i++) { + PersonWithEtag updatedPersonWithEtag = updatedPeople.get(i); + PersonWithEtag foundPersonWithEtag = foundPeople.get(i); + Assert.assertNotNull(foundPersonWithEtag.getEtag()); + Assert.assertEquals(updatedPersonWithEtag.getEtag(), foundPersonWithEtag.getEtag()); + } + } + + private List toList(Iterable people) { + return StreamSupport.stream(people.spliterator(), false) + .collect(Collectors.toList()); + } + + @Test + public void testShouldFailIfEtagDoesNotMatch() { + PersonWithEtag insertedPersonWithEtag = personWithEtagRepository.save(createPersonWithEtag()); + insertedPersonWithEtag.setFirstName(LAST_NAME); + + PersonWithEtag updatedPersonWithEtag = personWithEtagRepository.save(insertedPersonWithEtag); + updatedPersonWithEtag.setEtag(insertedPersonWithEtag.getEtag()); + + try { + personWithEtagRepository.save(updatedPersonWithEtag); + Assert.fail(); + } catch (CosmosAccessException ex) { + } + + try { + personWithEtagRepository.delete(updatedPersonWithEtag); + Assert.fail(); + } catch (CosmosAccessException ex) { + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/IndexPolicyUpdateIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/IndexPolicyUpdateIT.java new file mode 100644 index 000000000000..1e9633a09446 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/IndexPolicyUpdateIT.java @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.implementation.ImplementationBridgeHelpers; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.ExcludedPath; +import com.azure.cosmos.models.IncludedPath; +import com.azure.cosmos.models.IndexingMode; +import com.azure.cosmos.models.IndexingPolicy; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.domain.ComplexIndexPolicyEntity; +import com.azure.spring.data.cosmos.domain.IndexPolicyOverwriteEntity; +import com.azure.spring.data.cosmos.domain.IndexPolicyEntity; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.azure.spring.data.cosmos.repository.support.SimpleCosmosRepository; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class IndexPolicyUpdateIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + CosmosTemplate template; + + @Autowired + ApplicationContext context; + + CosmosEntityInformation defaultIndexPolicyEntityInformation = new CosmosEntityInformation<>(IndexPolicyEntity.class); + + CosmosEntityInformation complexIndexPolicyEntityInformation = new CosmosEntityInformation<>(ComplexIndexPolicyEntity.class); + + CosmosEntityInformation indexPolicyOverwriteEntityInformation = new CosmosEntityInformation<>(IndexPolicyOverwriteEntity.class); + + CosmosEntityInformation addressEntityInformation = new CosmosEntityInformation<>(Address.class); + + @Before + public void setup() { + collectionManager.ensureContainersCreatedAndEmpty(template, IndexPolicyEntity.class, ComplexIndexPolicyEntity.class, IndexPolicyOverwriteEntity.class, Address.class); + } + + @Test + public void testIndexPolicyDoesntUpdateOnRepoInitialization() { + // set index policy from entity annotation + collectionManager.deleteContainer(defaultIndexPolicyEntityInformation); + new SimpleCosmosRepository<>(defaultIndexPolicyEntityInformation, template); + + // get original index policy + CosmosContainerProperties properties = template.getContainerProperties(defaultIndexPolicyEntityInformation.getContainerName()); + + // assert + assertThat(properties.getIndexingPolicy().getIncludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getIncludedPaths().get(0).getPath()).isEqualTo("/*"); + assertThat(properties.getIndexingPolicy().getExcludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getExcludedPaths().get(0).getPath()).isEqualTo("/\"_etag\"/?"); + assertThat(properties.getIndexingPolicy().isAutomatic()).isEqualTo(true); + assertThat(properties.getIndexingPolicy().getIndexingMode()).isEqualTo(IndexingMode.CONSISTENT); + + // set new index policy + IndexingPolicy newIndexPolicy = new IndexingPolicy(); + newIndexPolicy.setIncludedPaths(Collections.singletonList(new IncludedPath("/field/?"))); + newIndexPolicy.setExcludedPaths(Collections.singletonList(new ExcludedPath("/*"))); + + // apply new index policy + CosmosEntityInformation spyEntityInformation = Mockito.spy(defaultIndexPolicyEntityInformation); + Mockito.doReturn(newIndexPolicy).when(spyEntityInformation).getIndexingPolicy(); + Mockito.doReturn(false).when(spyEntityInformation).isOverwriteIndexingPolicy(); + new SimpleCosmosRepository<>(spyEntityInformation, template); + + // retrieve updated index policy + properties = template.getContainerProperties(defaultIndexPolicyEntityInformation.getContainerName()); + + // assert + assertThat(properties.getIndexingPolicy().getIncludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getIncludedPaths().get(0).getPath()).isEqualTo("/*"); + assertThat(properties.getIndexingPolicy().getExcludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getExcludedPaths().get(0).getPath()).isEqualTo("/\"_etag\"/?"); + assertThat(properties.getIndexingPolicy().isAutomatic()).isEqualTo(true); + assertThat(properties.getIndexingPolicy().getIndexingMode()).isEqualTo(IndexingMode.CONSISTENT); + } + + @Test + public void testIndexPolicyUpdatesOnRepoInitialization() { + // set index policy from entity annotation + collectionManager.deleteContainer(indexPolicyOverwriteEntityInformation); + new SimpleCosmosRepository<>(indexPolicyOverwriteEntityInformation, template); + + // get original index policy + CosmosContainerProperties properties = template.getContainerProperties(indexPolicyOverwriteEntityInformation.getContainerName()); + + // assert + assertThat(properties.getIndexingPolicy().getIncludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getIncludedPaths().get(0).getPath()).isEqualTo("/\"_etag\"/?"); + assertThat(properties.getIndexingPolicy().getExcludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getExcludedPaths().get(0).getPath()).isEqualTo("/*"); + + // set new index policy + IndexingPolicy newIndexPolicy = new IndexingPolicy(); + newIndexPolicy.setIncludedPaths(Collections.singletonList(new IncludedPath("/*"))); + newIndexPolicy.setExcludedPaths(Collections.singletonList(new ExcludedPath("/\"_etag\"/?"))); + + // apply new index policy + CosmosEntityInformation spyEntityInformation = Mockito.spy(indexPolicyOverwriteEntityInformation); + Mockito.doReturn(newIndexPolicy).when(spyEntityInformation).getIndexingPolicy(); + Mockito.doReturn(true).when(spyEntityInformation).isOverwriteIndexingPolicy(); + new SimpleCosmosRepository<>(spyEntityInformation, template); + + // retrieve updated index policy + properties = template.getContainerProperties(indexPolicyOverwriteEntityInformation.getContainerName()); + + // assert + assertThat(properties.getIndexingPolicy().getIncludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getIncludedPaths().get(0).getPath()).isEqualTo("/*"); + assertThat(properties.getIndexingPolicy().getExcludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getExcludedPaths().get(0).getPath()).isEqualTo("/\"_etag\"/?"); + } + + @Test + public void testContainerReplaceShouldNotOccurIfDefaultIndexIsUnchanged() { + new SimpleCosmosRepository<>(defaultIndexPolicyEntityInformation, template); + CosmosTemplate spyTemplate = Mockito.spy(template); + new SimpleCosmosRepository<>(defaultIndexPolicyEntityInformation, spyTemplate); + Mockito.verify(spyTemplate, Mockito.never()).replaceContainerProperties(Mockito.any(), Mockito.any()); + } + + @Test + public void testContainerReplaceShouldNotOccurIfComplexIndexIsUnchanged() { + new SimpleCosmosRepository<>(complexIndexPolicyEntityInformation, template); + CosmosTemplate spyTemplate = Mockito.spy(template); + new SimpleCosmosRepository<>(complexIndexPolicyEntityInformation, spyTemplate); + Mockito.verify(spyTemplate, Mockito.never()).replaceContainerProperties(Mockito.any(), Mockito.any()); + } + + @Test + public void testContainerReplaceShouldNotOccurIfIndexingPolicyIsNotSpecified() { + new SimpleCosmosRepository<>(addressEntityInformation, template); + CosmosTemplate spyTemplate = Mockito.spy(template); + new SimpleCosmosRepository<>(addressEntityInformation, spyTemplate); + Mockito.verify(spyTemplate, Mockito.never()).replaceContainerProperties(Mockito.any(), Mockito.any()); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/IntegerIdDomainRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/IntegerIdDomainRepositoryIT.java new file mode 100644 index 000000000000..3ab41e93f45b --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/IntegerIdDomainRepositoryIT.java @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.domain.IntegerIdDomain; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.IntegerIdDomainRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +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); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private IntegerIdDomainRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, IntegerIdDomain.class); + this.repository.save(DOMAIN); + } + + @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 + 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 = CosmosAccessException.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 = CosmosAccessException.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(other, page2Iterator.next()); + } + + private static class InvalidDomain { + + private int count; + + private String location; + + InvalidDomain() { + } + + 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/LongIdDomainRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/LongIdDomainRepositoryIT.java new file mode 100644 index 000000000000..e628ecfba6c1 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/LongIdDomainRepositoryIT.java @@ -0,0 +1,284 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.domain.LongIdDomain; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.LongIdDomainRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class LongIdDomainRepositoryIT { + + private static final Long ID_1 = 12345L; + private static final String NAME_1 = "moary"; + + private static final Long ID_2 = 67890L; + private static final String NAME_2 = "camille"; + + private static final LongIdDomain DOMAIN_1 = new LongIdDomain(ID_1, NAME_1); + private static final LongIdDomain DOMAIN_2 = new LongIdDomain(ID_2, NAME_2); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private LongIdDomainRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, LongIdDomain.class); + this.repository.save(DOMAIN_1); + this.repository.save(DOMAIN_2); + } + + @Test + public void testLongIdDomain() { + this.repository.deleteAll(); + Assert.assertFalse(this.repository.findById(ID_1).isPresent()); + + this.repository.save(DOMAIN_1); + final Optional foundOptional = this.repository.findById(ID_1); + + Assert.assertTrue(foundOptional.isPresent()); + Assert.assertEquals(DOMAIN_1.getNumber(), foundOptional.get().getNumber()); + Assert.assertEquals(DOMAIN_1.getName(), foundOptional.get().getName()); + + this.repository.delete(DOMAIN_1); + + Assert.assertFalse(this.repository.findById(ID_1).isPresent()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidDomain() { + new CosmosEntityInformation(InvalidDomain.class); + } + + @Test + public void testBasicQuery() { + final LongIdDomain save = this.repository.save(DOMAIN_1); + Assert.assertNotNull(save); + } + + @Test + public void testSaveAndFindById() { + Assert.assertNotNull(this.repository.save(DOMAIN_1)); + + final Optional savedEntity = this.repository.findById(DOMAIN_1.getNumber()); + Assert.assertTrue(savedEntity.isPresent()); + Assert.assertEquals(DOMAIN_1, savedEntity.get()); + } + + @Test + public void testSaveAllAndFindAll() { + Assert.assertTrue(this.repository.findAll().iterator().hasNext()); + + final Set entitiesToSave = Stream.of(DOMAIN_1, DOMAIN_2).collect(Collectors.toSet()); + this.repository.saveAll(entitiesToSave); + + final Set savedEntities = StreamSupport.stream(this.repository.findAll().spliterator(), false) + .collect(Collectors.toSet()); + + Assert.assertTrue(entitiesToSave.containsAll(savedEntities)); + } + + @Test + public void testFindAllById() { + final Iterable allById = + TestUtils.toList(this.repository.findAllById(Arrays.asList(DOMAIN_1.getNumber(), DOMAIN_2.getNumber()))); + Assert.assertTrue(((ArrayList) allById).size() == 2); + Iterator it = allById.iterator(); + assertLongIdDomainEquals(Arrays.asList(it.next(), it.next()), Arrays.asList(DOMAIN_1, DOMAIN_2)); + } + + private void assertLongIdDomainEquals(List cur, List reference) { + cur.sort(Comparator.comparing(LongIdDomain::getNumber)); + reference.sort(Comparator.comparing(LongIdDomain::getNumber)); + Assert.assertEquals(reference, cur); + } + + @Test + public void testCount() { + Assert.assertEquals(2, repository.count()); + } + + @Test + public void testDeleteById() { + this.repository.save(DOMAIN_1); + this.repository.save(DOMAIN_2); + this.repository.deleteById(DOMAIN_1.getNumber()); + this.repository.deleteById(DOMAIN_2.getNumber()); + Assert.assertEquals(0, this.repository.count()); + } + + @Test(expected = CosmosAccessException.class) + public void testDeleteByIdShouldFailIfNothingToDelete() { + this.repository.deleteAll(); + this.repository.deleteById(DOMAIN_1.getNumber()); + } + + @Test + public void testDelete() { + this.repository.save(DOMAIN_1); + this.repository.delete(DOMAIN_1); + Assert.assertEquals(1, this.repository.count()); + } + + @Test(expected = CosmosAccessException.class) + public void testDeleteShouldFailIfNothingToDelete() { + this.repository.deleteAll(); + this.repository.delete(DOMAIN_1); + } + + @Test + public void testDeleteAll() { + this.repository.save(DOMAIN_1); + this.repository.save(DOMAIN_2); + this.repository.deleteAll(Arrays.asList(DOMAIN_1, DOMAIN_2)); + Assert.assertEquals(0, this.repository.count()); + } + + @Test + public void testExistsById() { + this.repository.save(DOMAIN_1); + Assert.assertTrue(this.repository.existsById(DOMAIN_1.getNumber())); + } + + @Test + public void testFindAllSort() { + final LongIdDomain other = new LongIdDomain(DOMAIN_1.getNumber() + 1, "other-name"); + this.repository.save(other); + this.repository.save(DOMAIN_1); + + 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(3, ascending.size()); + Assert.assertEquals(DOMAIN_1, ascending.get(0)); + Assert.assertEquals(other, ascending.get(1)); + Assert.assertEquals(DOMAIN_2, ascending.get(2)); + + 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(3, descending.size()); + Assert.assertEquals(DOMAIN_2, descending.get(0)); + Assert.assertEquals(other, descending.get(1)); + Assert.assertEquals(DOMAIN_1, descending.get(2)); + + } + + @Test + public void testFindAllPageable() { + final LongIdDomain other = new LongIdDomain(DOMAIN_1.getNumber() + 1, "other-name"); + 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_1, 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_2, page2Iterator.next()); + + final Page page3 = this.repository.findAll(new CosmosPageRequest(2, 1, null)); + final Iterator page3Iterator = page3.iterator(); + Assert.assertTrue(page3Iterator.hasNext()); + Assert.assertEquals(other, page3Iterator.next()); + } + + private static class InvalidDomain { + + private long count; + + private String location; + + InvalidDomain() { + } + + InvalidDomain(long count, String location) { + this.count = count; + this.location = location; + } + + public long getCount() { + return count; + } + + public void setCount(long 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/MemoRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/MemoRepositoryIT.java new file mode 100644 index 000000000000..a032499c9979 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/MemoRepositoryIT.java @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Importance; +import com.azure.spring.data.cosmos.domain.Memo; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.MemoRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +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.Collections; +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; + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @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() { + collectionManager.ensureContainersCreatedAndEmpty(template, Memo.class); + repository.saveAll(Arrays.asList(testMemo1, testMemo2, testMemo3)); + } + + @Test + public void testFindAll() { + final List result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(3); + } + + @Test + public void testFindByDate() { + final List result = TestUtils.toList(repository.findMemoByDate(memoDate)); + + assertThat(result.size()).isEqualTo(1); + assertMemoEquals(result.get(0), testMemo1); + } + + @Test + public void testFindByEnum() { + final List result = TestUtils.toList(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 = TestUtils.toList(this.repository.findByDateBefore(memoDateBefore)); + + Assert.assertTrue(memos.isEmpty()); + + memos = TestUtils.toList(this.repository.findByDateBefore(memoDate)); + + Assert.assertEquals(1, memos.size()); + Assert.assertEquals(testMemo1, memos.get(0)); + + memos = TestUtils.toList(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 = TestUtils.toList(this.repository.findByDateBeforeAndMessage(memoDate, + TestConstants.NEW_MESSAGE)); + + Assert.assertTrue(memos.isEmpty()); + + memos = TestUtils.toList(this.repository.findByDateBeforeAndMessage(memoDate, TestConstants.MESSAGE)); + + Assert.assertEquals(1, memos.size()); + Assert.assertEquals(testMemo1, memos.get(0)); + + memos = TestUtils.toList(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 = TestUtils.toList(this.repository.findByDateAfter(memoDateAfter)); + + Assert.assertTrue(memos.isEmpty()); + + memos = TestUtils.toList(this.repository.findByDateAfter(memoDate)); + + Assert.assertEquals(1, memos.size()); + Assert.assertEquals(testMemo3, memos.get(0)); + + memos = TestUtils.toList(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 = TestUtils.toList(this.repository.findByDateAfterAndMessage(memoDate, TestConstants.MESSAGE)); + + Assert.assertTrue(memos.isEmpty()); + + memos = TestUtils.toList(this.repository.findByDateAfterAndMessage(memoDate, TestConstants.NEW_MESSAGE)); + + Assert.assertEquals(1, memos.size()); + Assert.assertEquals(testMemo3, memos.get(0)); + + memos = TestUtils.toList(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 = TestUtils.toList(this.repository + .findByDateBetween(testMemo1.getDate(), testMemo3.getDate())); + List reference = Arrays.asList(testMemo1, testMemo2, testMemo3); + + assertMemoListEquals(memos, reference); + + memos = TestUtils.toList(this.repository.findByDateBetween(testMemo1.getDate(), testMemo2.getDate())); + reference = Arrays.asList(testMemo1, testMemo2); + + assertMemoListEquals(memos, reference); + + memos = TestUtils.toList(this.repository.findByDateBetween(futureDate1, futureDate2)); + reference = Collections.emptyList(); + + assertMemoListEquals(memos, reference); + } + + @Test + public void testFindByBetweenWithAnd() { + final List memos = TestUtils.toList(this.repository + .findByDateBetweenAndMessage(testMemo1.getDate(), testMemo2.getDate(), TestConstants.MESSAGE)); + assertMemoListEquals(memos, Collections.singletonList(testMemo1)); + } + + @Test + public void testFindByBetweenWithOr() { + final List memos = TestUtils.toList(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 = CosmosAccessException.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 = + TestUtils.toList(repository.findByMessageStartsWith(testMemo1.getMessage().substring(0, 10))); + Assert.assertEquals(testMemo1, result.get(0)); + Assert.assertEquals(1, result.size()); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/NestedPartitionKeyRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/NestedPartitionKeyRepositoryIT.java new file mode 100644 index 000000000000..5e2515c27cef --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/NestedPartitionKeyRepositoryIT.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.NestedEntity; +import com.azure.spring.data.cosmos.domain.NestedPartitionKeyEntityWithGeneratedValue; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.NestedPartitionKeyRepository; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +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.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class NestedPartitionKeyRepositoryIT { + + private static final NestedPartitionKeyEntityWithGeneratedValue NESTED_ENTITY_1 = + new NestedPartitionKeyEntityWithGeneratedValue(null, new NestedEntity("partitionKey1")); + + private static final NestedPartitionKeyEntityWithGeneratedValue NESTED_ENTITY_2 = + new NestedPartitionKeyEntityWithGeneratedValue(null, new NestedEntity("partitionKey2")); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + NestedPartitionKeyRepository repository; + + @BeforeClass + public static void init() { } + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, NestedPartitionKeyEntityWithGeneratedValue.class); + } + + @Test + public void testSave() { + NestedPartitionKeyEntityWithGeneratedValue savedEntity = repository.save(NESTED_ENTITY_1); + + assertThat(savedEntity).isEqualTo(NESTED_ENTITY_1); + } + + @Test + public void testFindAll() { + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + Iterable iterable = repository.findAll(); + List nestedPartitionKeyEntityWithGeneratedValues = + TestUtils.toList(iterable); + assertThat(nestedPartitionKeyEntityWithGeneratedValues.size()).isEqualTo(2); + } + + @Test + public void testFindAllByPartitionKey() { + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + Iterable partitionKey = repository.findAll(new PartitionKey( + "partitionKey2")); + assertThat(partitionKey.iterator().hasNext()).isTrue(); + assertThat(partitionKey.iterator().next()).isEqualTo(NESTED_ENTITY_2); + } + + @Test + public void testFindByIdAndPartitionKey() { + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + Optional nestedEntity = repository.findById(NESTED_ENTITY_1.getId(), + new PartitionKey(NESTED_ENTITY_1.getNestedEntity().getNestedPartitionKey())); + assertThat(nestedEntity.isPresent()).isTrue(); + assertThat(nestedEntity.get()).isEqualTo(NESTED_ENTITY_1); + } + + @Test + public void testDeleteByIdAndPartitionKey() { + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + repository.deleteById(NESTED_ENTITY_1.getId(), new PartitionKey(NESTED_ENTITY_1.getNestedEntity().getNestedPartitionKey())); + Iterable iterable = repository.findAll(); + List nestedPartitionKeyEntityWithGeneratedValues = + TestUtils.toList(iterable); + assertThat(nestedPartitionKeyEntityWithGeneratedValues.size()).isEqualTo(1); + assertThat(nestedPartitionKeyEntityWithGeneratedValues.get(0)).isEqualTo(NESTED_ENTITY_2); + } + + @Test + public void testDeleteByEntity() { + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + repository.delete(NESTED_ENTITY_1); + Iterable iterable = repository.findAll(); + List nestedPartitionKeyEntityWithGeneratedValues = + TestUtils.toList(iterable); + assertThat(nestedPartitionKeyEntityWithGeneratedValues.size()).isEqualTo(1); + assertThat(nestedPartitionKeyEntityWithGeneratedValues.get(0)).isEqualTo(NESTED_ENTITY_2); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageableAddressRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageableAddressRepositoryIT.java new file mode 100644 index 000000000000..18707edbe276 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageableAddressRepositoryIT.java @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.models.CosmosQueryRequestOptions; +import com.azure.cosmos.models.FeedResponse; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.PageableAddressRepository; +import org.junit.Before; +import org.junit.ClassRule; +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.azure.spring.data.cosmos.common.PageTestUtils.validateLastPage; +import static com.azure.spring.data.cosmos.common.PageTestUtils.validateNonLastPage; +import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_1; +import static com.azure.spring.data.cosmos.common.TestConstants.PAGE_SIZE_3; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION1; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS1_PARTITION2; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS2_PARTITION1; +import static com.azure.spring.data.cosmos.domain.Address.TEST_ADDRESS4_PARTITION3; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class PageableAddressRepositoryIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private PageableAddressRepository repository; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private CosmosFactory cosmosFactory; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Address.class); + repository.save(TEST_ADDRESS1_PARTITION1); + repository.save(TEST_ADDRESS1_PARTITION2); + repository.save(TEST_ADDRESS2_PARTITION1); + repository.save(TEST_ADDRESS4_PARTITION3); + repository.save(new Address(TestConstants.POSTAL_CODE, TestConstants.STREET, TestConstants.CITY_0)); + } + + @Test + public void testFindAll() { + final List
result = TestUtils.toList(repository.findAll()); + + assertThat(result.size()).isEqualTo(5); + } + + @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.nextPageable()); + assertThat(nextPage.getContent().size()).isLessThanOrEqualTo(PAGE_SIZE_3); + validateLastPage(nextPage, PAGE_SIZE_3); + } + + @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_SIZE_3); + } + + @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.nextPageable()); + + 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_SIZE_3); + } + + @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.nextPageable()); + + 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 CosmosQueryRequestOptions options = new CosmosQueryRequestOptions(); + options.setMaxDegreeOfParallelism(2); + + final String query = "SELECT * from c OFFSET " + skipCount + " LIMIT " + takeCount; + + final CosmosAsyncClient cosmosAsyncClient = applicationContext.getBean(CosmosAsyncClient.class); + final Flux> feedResponseFlux = + cosmosAsyncClient.getDatabase(cosmosFactory.getDatabaseName()) + .getContainer(collectionManager.getContainerName(Address.class)) + .queryItems(query, options, Address.class) + .byPage(); + + StepVerifier.create(feedResponseFlux) + .consumeNextWith(cosmosItemPropertiesFeedResponse -> + results.addAll(cosmosItemPropertiesFeedResponse.getResults())) + .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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageableMemoRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageableMemoRepositoryIT.java new file mode 100644 index 000000000000..7cf66204ec4e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageableMemoRepositoryIT.java @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.models.CosmosQueryRequestOptions; +import com.azure.cosmos.models.FeedResponse; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.domain.Importance; +import com.azure.spring.data.cosmos.domain.PageableMemo; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.PageableMemoRepository; +import org.junit.Before; +import org.junit.ClassRule; +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.data.domain.Slice; +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 java.util.stream.Collectors; + +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; + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private PageableMemoRepository repository; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private CosmosFactory cosmosFactory; + + private static Set memoSet; + private static Set normalMemos; + + private static boolean isSetupDone; + + @Before + public void setUp() { + collectionManager.ensureContainersCreated(template, PageableMemo.class); + + if (isSetupDone) { + return; + } + 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); + } + + // Set of memos with NORMAL importance + normalMemos = memoSet.stream() + .filter(m -> m.getImportance().equals(Importance.NORMAL)) + .collect(Collectors.toSet()); + + isSetupDone = true; + } + + @Test + public void testFindAllWithPageSizeLessThanReturned() { + final Set memos = findAllWithPageSize(20); + boolean equal = memos.equals(memoSet); + assertThat(equal).isTrue(); + } + + @Test + public void testFindAllWithPageSizeLessThanTotal() { + final Set memos = findAllWithPageSize(200); + boolean equal = memos.equals(memoSet); + assertThat(equal).isTrue(); + } + + @Test + public void testFindAllWithPageSizeGreaterThanTotal() { + final Set memos = findAllWithPageSize(10000); + boolean equal = memos.equals(memoSet); + assertThat(equal).isTrue(); + } + + @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); + } + + @Test + public void testFindByImportanceUsingSliceWithPageSizeLessThanReturned() { + final Set memos = findByWithSlice(20); + boolean equal = memos.equals(normalMemos); + assertThat(equal).isTrue(); + } + + @Test + public void testFindByImportanceUsingSliceWithPageSizeLessThanTotal() { + final Set memos = findByWithSlice(200); + boolean equal = memos.equals(normalMemos); + assertThat(equal).isTrue(); + } + + @Test + public void testFindByImportanceUsingSliceWithPageSizeGreaterThanTotal() { + final Set memos = findByWithSlice(10000); + boolean equal = memos.equals(normalMemos); + assertThat(equal).isTrue(); + } + + private Flux> getItemsWithOffsetAndLimit(int skipCount, int takeCount) { + final CosmosQueryRequestOptions options = new CosmosQueryRequestOptions(); + options.setMaxDegreeOfParallelism(2); + + final String query = "SELECT * from c OFFSET " + skipCount + " LIMIT " + takeCount; + + final CosmosAsyncClient cosmosAsyncClient = applicationContext.getBean(CosmosAsyncClient.class); + return cosmosAsyncClient.getDatabase(cosmosFactory.getDatabaseName()) + .getContainer(collectionManager.getContainerName(PageableMemo.class)) + .queryItems(query, options, PageableMemo.class) + .byPage(); + } + + 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.getResults()); + 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; + } + + private Set findByWithSlice(int pageSize) { + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, pageSize, null); + Slice slice = repository.findByImportance(Importance.NORMAL, pageRequest); + final Set outputSet = new HashSet<>(slice.getContent()); + while (slice.hasNext()) { + final Pageable pageable = slice.nextPageable(); + slice = repository.findByImportance(Importance.NORMAL, pageable); + outputSet.addAll((slice.getContent())); + } + return outputSet; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageablePersonRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageablePersonRepositoryIT.java new file mode 100644 index 000000000000..e28702e04731 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PageablePersonRepositoryIT.java @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.domain.PageablePerson; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.PageablePersonRepository; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.ClassRule; +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 = 25; + public static final int ONE_KB = 1024; + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private PageablePersonRepository repository; + + private static Set personSet; + + private static boolean isSetupDone; + + @Before + public void setUp() { + collectionManager.ensureContainersCreated(template, PageablePerson.class); + + if (isSetupDone) { + return; + } + personSet = new HashSet<>(); + + // 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(), + 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; + } + + // 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 12 documents. + + // This test covers the case where page size is less than returned documents limit + @Test + public void testFindAllWithPageSizeLessThanReturnedLimit() { + final Set outputSet = findAllWithPageSize(5, false); + boolean equals = outputSet.equals(personSet); + assertThat(equals).isTrue(); + } + + // This test covers the case where page size is greater than total number of documents + @Test + public void testFindAllWithPageSizeGreaterThanTotal() { + final Set outputSet = findAllWithPageSize(50, true); + boolean equals = outputSet.equals(personSet); + assertThat(equals).isTrue(); + } + + private Set findAllWithPageSize(int pageSize, boolean checkContentLimit) { + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, pageSize, null); + Page page = repository.findAll(pageRequest); + List content = page.getContent(); + final Set outputSet = new HashSet<>(content); + if (checkContentLimit) { + // Make sure CosmosDB returns less number of documents than requested + // This will verify the functionality of new pagination implementation + assertThat(content.size()).isLessThan(pageSize); + } + while (page.hasNext()) { + final Pageable pageable = page.nextPageable(); + page = repository.findAll(pageable); + content = page.getContent(); + outputSet.addAll((content)); + } + return outputSet; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PersistableIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PersistableIT.java new file mode 100644 index 000000000000..2bf11ff8779e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PersistableIT.java @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.implementation.ConflictException; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.PersistableEntity; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.PersistableEntityRepository; +import com.azure.spring.data.cosmos.repository.repository.ReactivePersistableEntityRepository; +import org.junit.Before; +import org.junit.ClassRule; +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 reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class PersistableIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private PersistableEntityRepository repository; + + @Autowired + private ReactivePersistableEntityRepository reactiveRepository; + + @Autowired + private CosmosTemplate template; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, PersistableEntity.class); + } + + @Test + public void testInsertShouldSaveEntity() { + final PersistableEntity entity = new PersistableEntity("id", "pk"); + assertTrue(entity.isNew()); + final PersistableEntity savedEntity = repository.save(entity); + assertNotNull(savedEntity.getVersion()); + } + + @Test + public void testReactiveInsertShouldSaveEntity() { + final PersistableEntity entity = new PersistableEntity("id", "pk"); + assertTrue(entity.isNew()); + final Mono savedEntity = reactiveRepository.save(entity); + StepVerifier.create(savedEntity).expectNext(entity).verifyComplete(); + } + + @Test + public void testInsertDuplicateShouldThrowConflictException() { + final PersistableEntity entity = new PersistableEntity("id", "pk"); + assertTrue(entity.isNew()); + repository.save(entity); + + try { + repository.save(entity); + fail("expecting conflict exception"); + } catch (CosmosAccessException ex) { + assertTrue(ex.getCosmosException() instanceof ConflictException); + } + } + + @Test + public void testReactiveInsertDuplicateShouldThrowConflictException() { + final PersistableEntity entity = new PersistableEntity("id", "pk"); + assertTrue(entity.isNew()); + reactiveRepository.save(entity).block(); + + final Mono saveSecond = reactiveRepository.save(entity); + StepVerifier.create(saveSecond) + .expectErrorMatches(ex -> ex instanceof CosmosAccessException && ((CosmosAccessException) ex).getCosmosException() instanceof ConflictException) + .verify(); + } + + @Test + public void testUpdateShouldSucceedEvenIfEntityDoesNotExist() { + final PersistableEntity entity = new PersistableEntity("id", "pk", "version"); + assertFalse(entity.isNew()); + + final PersistableEntity savedEntity = repository.save(entity); + assertNotEquals(entity.getVersion(), savedEntity.getVersion()); + } + + @Test + public void testReactiveUpdateShouldSucceedEvenIfEntityDoesNotExist() { + final PersistableEntity entity = new PersistableEntity("id", "pk", "version"); + assertFalse(entity.isNew()); + + final Mono savedMono = reactiveRepository.save(entity); + StepVerifier.create(savedMono) + .assertNext(savedEntity -> assertNotEquals(entity.getVersion(), savedEntity.getVersion())) + .verifyComplete(); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PersonCrossPartitionIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PersonCrossPartitionIT.java new file mode 100644 index 000000000000..472807cc2e02 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/PersonCrossPartitionIT.java @@ -0,0 +1,287 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.PageTestUtils; +import com.azure.spring.data.cosmos.common.ResponseDiagnosticsTestUtils; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.domain.PersonCrossPartition; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.AddressRepository; +import com.azure.spring.data.cosmos.repository.repository.AuditableRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.assertj.core.util.Lists; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.ClassRule; +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.ArrayList; +import java.util.List; + +import static com.azure.spring.data.cosmos.common.TestConstants.ADDRESSES; +import static com.azure.spring.data.cosmos.common.TestConstants.AGE; +import static com.azure.spring.data.cosmos.common.TestConstants.FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.HOBBIES; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_1; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_2; +import static com.azure.spring.data.cosmos.common.TestConstants.ID_3; +import static com.azure.spring.data.cosmos.common.TestConstants.LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_FIRST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.NEW_LAST_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.PASSPORT_IDS_BY_COUNTRY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.domain.Sort.Direction.ASC; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class PersonCrossPartitionIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + private static final PersonCrossPartition TEST_PERSON_CP = new PersonCrossPartition(ID_1, FIRST_NAME, LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + private static final PersonCrossPartition TEST_PERSON_CP_2 = new PersonCrossPartition(ID_2, NEW_FIRST_NAME, NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + private static final PersonCrossPartition TEST_PERSON_CP_3 = new PersonCrossPartition(ID_3, NEW_FIRST_NAME, NEW_LAST_NAME, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + + @Autowired + private ResponseDiagnosticsTestUtils responseDiagnosticsTestUtils; + + @Autowired + private CosmosTemplate cosmosTemplate; + + private static CosmosEntityInformation personCrossPartitionInfo; + + private static String containerName; + + @Autowired + private AuditableRepository auditableRepository; + + @Autowired + private AddressRepository addressRepository; + + @Before + public void setUp() { + personCrossPartitionInfo = new CosmosEntityInformation<>(PersonCrossPartition.class); + containerName = personCrossPartitionInfo.getContainerName(); + + collectionManager.ensureContainersCreatedAndEmpty(cosmosTemplate, PersonCrossPartition.class); + } + + @AfterClass + public static void cleanUp() { + collectionManager.deleteContainer(personCrossPartitionInfo); + } + + @Test + public void testFindAllPageableMultiPagesMultiPartition() { + cosmosTemplate.insert(TEST_PERSON_CP, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP))); + cosmosTemplate.insert(TEST_PERSON_CP_2, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP_2))); + cosmosTemplate.insert(TEST_PERSON_CP_3, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP_3))); + final List expected = Lists.newArrayList(TEST_PERSON_CP, TEST_PERSON_CP_2, TEST_PERSON_CP_3); + + for (int i=4; i<=10; i++) { + PersonCrossPartition temp = new PersonCrossPartition("id_" + i, "fred", LAST_NAME + "_" + i, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + cosmosTemplate.insert(temp, new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(temp))); + expected.add(temp); + } + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, 100, null); + final Page page1 = cosmosTemplate.findAll(pageRequest, PersonCrossPartition.class, containerName); + + final List resultPage1 = TestUtils.toList(page1); + assertThat(resultPage1.size()).isEqualTo(expected.size()); + assertThat(resultPage1).containsAll(expected); + PageTestUtils.validateLastPage(page1, 100); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + } + + @Test + public void testFindAllPageableMultiPagesMultiPartition2() { + cosmosTemplate.insert(TEST_PERSON_CP, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP))); + cosmosTemplate.insert(TEST_PERSON_CP_2, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP_2))); + cosmosTemplate.insert(TEST_PERSON_CP_3, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP_3))); + final List expected = Lists.newArrayList(TEST_PERSON_CP, TEST_PERSON_CP_2, TEST_PERSON_CP_3); + + for (int i=4; i<=10; i++) { + PersonCrossPartition temp = new PersonCrossPartition("id_" + i, "fred", LAST_NAME + "_" + i, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + cosmosTemplate.insert(temp, new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(temp))); + expected.add(temp); + } + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, 7, null); + final Page page1 = cosmosTemplate.findAll(pageRequest, PersonCrossPartition.class, containerName); + + final List resultPage1 = TestUtils.toList(page1); + assertThat(resultPage1.size()).isEqualTo(7); + PageTestUtils.validateNonLastPage(page1, 7); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final Page page2 = cosmosTemplate.findAll(page1.nextPageable(), PersonCrossPartition.class, containerName); + + final List resultPage2 = TestUtils.toList(page2); + assertThat(resultPage2.size()).isEqualTo(3); + PageTestUtils.validateLastPage(page2, 7); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final List allResults = new ArrayList<>(); + allResults.addAll(resultPage1); + allResults.addAll(resultPage2); + assertThat(allResults).containsAll(expected); + } + + @Test + public void testFindAllPageableMultiPagesMultiPartition3() { + cosmosTemplate.insert(TEST_PERSON_CP, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP))); + cosmosTemplate.insert(TEST_PERSON_CP_2, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP_2))); + cosmosTemplate.insert(TEST_PERSON_CP_3, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP_3))); + final List expected = Lists.newArrayList(TEST_PERSON_CP, TEST_PERSON_CP_2, TEST_PERSON_CP_3); + + for (int i=4; i<=10; i++) { + PersonCrossPartition temp = new PersonCrossPartition("id_" + i, "fred", LAST_NAME + "_" + i, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + cosmosTemplate.insert(temp, new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(temp))); + expected.add(temp); + } + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, 3, null); + final Page page1 = cosmosTemplate.findAll(pageRequest, PersonCrossPartition.class, containerName); + + final List resultPage1 = TestUtils.toList(page1); + assertThat(resultPage1.size()).isEqualTo(3); + PageTestUtils.validateNonLastPage(page1, 3); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final Page page2 = cosmosTemplate.findAll(page1.nextPageable(), PersonCrossPartition.class, containerName); + + final List resultPage2 = TestUtils.toList(page2); + assertThat(resultPage2.size()).isEqualTo(3); + PageTestUtils.validateNonLastPage(page2, 3); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final Page page3 = cosmosTemplate.findAll(page2.nextPageable(), PersonCrossPartition.class, containerName); + + final List resultPage3 = TestUtils.toList(page3); + assertThat(resultPage3.size()).isEqualTo(3); + PageTestUtils.validateNonLastPage(page3, 3); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final Page page4 = cosmosTemplate.findAll(page3.nextPageable(), PersonCrossPartition.class, containerName); + + final List resultPage4 = TestUtils.toList(page4); + assertThat(resultPage4.size()).isEqualTo(1); + PageTestUtils.validateLastPage(page4, 3); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final List allResults = new ArrayList<>(); + allResults.addAll(resultPage1); + allResults.addAll(resultPage2); + allResults.addAll(resultPage3); + allResults.addAll(resultPage4); + assertThat(allResults).containsAll(expected); + } + + @Test + public void testFindAllPageableMultiPagesMultiPartitionWithOffset() { + cosmosTemplate.insert(TEST_PERSON_CP, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP))); + cosmosTemplate.insert(TEST_PERSON_CP_2, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP_2))); + cosmosTemplate.insert(TEST_PERSON_CP_3, + new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(TEST_PERSON_CP_3))); + final List expected = Lists.newArrayList(TEST_PERSON_CP_2, TEST_PERSON_CP_3); + + for (int i=4; i<=10; i++) { + PersonCrossPartition temp = new PersonCrossPartition("id_" + i, "fred", LAST_NAME + "_" + i, HOBBIES, + ADDRESSES, AGE, PASSPORT_IDS_BY_COUNTRY); + cosmosTemplate.insert(temp, new PartitionKey(personCrossPartitionInfo.getPartitionKeyFieldValue(temp))); + expected.add(temp); + } + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNull(); + + final CosmosPageRequest pageRequest = CosmosPageRequest.of(1, 0, 7, + null, Sort.by(ASC, "id")); + final Page page1 = cosmosTemplate.findAll(pageRequest, PersonCrossPartition.class, containerName); + + final List resultPage1 = TestUtils.toList(page1); + assertThat(resultPage1.size()).isEqualTo(7); + PageTestUtils.validateNonLastPage(page1, 7); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final Page page2 = cosmosTemplate.findAll(page1.nextPageable(), PersonCrossPartition.class, containerName); + + final List resultPage2 = TestUtils.toList(page2); + assertThat(resultPage2.size()).isEqualTo(2); + PageTestUtils.validateLastPage(page2, 7); + + assertThat(responseDiagnosticsTestUtils.getCosmosDiagnostics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics()).isNotNull(); + assertThat(responseDiagnosticsTestUtils.getCosmosResponseStatistics().getRequestCharge()).isGreaterThan(0); + + final List allResults = new ArrayList<>(); + allResults.addAll(resultPage1); + allResults.addAll(resultPage2); + assertThat(allResults).containsAll(expected); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ProjectRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ProjectRepositoryIT.java new file mode 100644 index 000000000000..78a1bba4a862 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ProjectRepositoryIT.java @@ -0,0 +1,563 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Project; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ProjectRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.ArrayList; +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; +import static org.junit.Assert.assertTrue; + +@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); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private ProjectRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Project.class); + this.repository.saveAll(PROJECTS); + } + + 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 = TestUtils.toList(this.repository.findByNameAndStarCount(NAME_1, STAR_COUNT_1)); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_1)); + + projects = TestUtils.toList(this.repository.findByNameAndStarCount(NAME_0, STAR_COUNT_1)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(this.repository.findByNameAndStarCount(NAME_0, STAR_COUNT_0)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + } + + @Test + public void testFindByWithOr() { + List projects = TestUtils.toList(this.repository.findByNameOrForkCount(NAME_2, STAR_COUNT_2)); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_2)); + + projects = TestUtils.toList(this.repository.findByNameOrForkCount(FAKE_NAME, FAKE_COUNT)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(this.repository.findByNameOrForkCount(NAME_0, FORK_COUNT_1)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_4)); + } + + @Test + public void testFindByWithAndPartition() { + List projects = TestUtils.toList(this.repository.findByNameAndCreator(NAME_1, CREATOR_1)); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_1)); + + projects = TestUtils.toList(this.repository.findByNameAndCreator(NAME_0, CREATOR_1)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(this.repository.findByNameAndCreator(NAME_0, CREATOR_0)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + } + + @Test + public void testFindByWithRepeatedParameters() { + List projects = TestUtils.toList(this.repository.findByNameAndCreatorOrNameAndCreator(NAME_1, CREATOR_1, NAME_2, CREATOR_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + } + + @Test + public void testFindByWithOrPartition() { + List projects = TestUtils.toList(this.repository.findByNameOrCreator(NAME_2, CREATOR_2)); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_2)); + + projects = TestUtils.toList(this.repository.findByNameOrCreator(FAKE_NAME, FAKE_CREATOR)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(this.repository.findByNameOrCreator(NAME_0, CREATOR_1)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_4)); + } + + @Test + public void testFindByWithAndOr() { + List projects = TestUtils.toList(repository.findByNameAndCreatorOrForkCount(NAME_0, CREATOR_1, + FORK_COUNT_2)); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_2)); + + projects = TestUtils.toList(repository.findByNameAndCreatorOrForkCount(NAME_1, CREATOR_2, FAKE_COUNT)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByNameAndCreatorOrForkCount(NAME_1, CREATOR_1, FORK_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + } + + @Test + public void testFindByWithOrAnd() { + List projects = TestUtils.toList(repository.findByNameOrCreatorAndForkCount(NAME_0, CREATOR_1, + FORK_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + + projects = TestUtils.toList(repository.findByNameOrCreatorAndForkCount(FAKE_NAME, CREATOR_1, FORK_COUNT_2)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByNameOrCreatorAndForkCount(NAME_1, CREATOR_2, FORK_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + } + + @Test + public void testFindByWithOrOr() { + List projects = TestUtils.toList(repository.findByNameOrCreatorOrForkCount(NAME_0, CREATOR_1, + FORK_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_4)); + + projects = TestUtils.toList(repository.findByNameOrCreatorOrForkCount(FAKE_NAME, FAKE_CREATOR, FAKE_COUNT)); + + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindByWithOrAndOr() { + List projects = TestUtils.toList(repository.findByNameOrCreatorAndForkCountOrStarCount(NAME_1, + CREATOR_0, + FORK_COUNT_2, STAR_COUNT_3)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_3)); + + projects = TestUtils.toList(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 = TestUtils.toList(repository.findByNameOrCreatorAndForkCountOrStarCount(FAKE_NAME, CREATOR_1, + FORK_COUNT_0, FAKE_COUNT)); + + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindByGreaterThan() { + List projects = TestUtils.toList(repository.findByForkCountGreaterThan(FORK_COUNT_1)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_2, PROJECT_3)); + + projects = TestUtils.toList(repository.findByForkCountGreaterThan(FAKE_COUNT)); + + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindByGreaterThanWithAndOr() { + List projects = TestUtils.toList(repository.findByCreatorAndForkCountGreaterThan(CREATOR_2, + FORK_COUNT_1)); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_2)); + + projects = TestUtils.toList(repository.findByCreatorAndForkCountGreaterThan(CREATOR_0, FORK_COUNT_1)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByCreatorOrForkCountGreaterThan(CREATOR_0, FORK_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_3, PROJECT_4)); + } + + @Test + public void testFindByLessThan() { + List projects = TestUtils.toList(repository.findByStarCountLessThan(STAR_COUNT_0)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByStarCountLessThan(STAR_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_4)); + } + + @Test + public void testFindByLessThanEqual() { + List projects = TestUtils.toList(repository.findByForkCountLessThanEqual(STAR_COUNT_MIN)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByForkCountLessThanEqual(STAR_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_4)); + } + + @Test + public void testFindByLessThanAndGreaterThan() { + List projects = + TestUtils.toList(repository.findByStarCountLessThanAndForkCountGreaterThan(STAR_COUNT_0, FORK_COUNT_3)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByStarCountLessThanAndForkCountGreaterThan(STAR_COUNT_3, + FORK_COUNT_0)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + } + + @Test + public void testFindByLessThanEqualsAndGreaterThanEquals() { + List projects = TestUtils.toList(repository.findByForkCountLessThanEqualAndStarCountGreaterThan( + STAR_COUNT_MIN, FORK_COUNT_0)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByForkCountLessThanEqualAndStarCountGreaterThan(STAR_COUNT_3, + FORK_COUNT_0)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2, PROJECT_3)); + } + + @Test + public void testFindByGreaterThanEqual() { + List projects = TestUtils.toList(repository.findByStarCountGreaterThanEqual(STAR_COUNT_MAX)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByStarCountGreaterThanEqual(STAR_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_2, PROJECT_3)); + } + + @Test + public void testFindByGreaterThanEqualAnd() { + List projects = TestUtils.toList(repository + .findByForkCountGreaterThanEqualAndCreator(FORK_COUNT_MAX, CREATOR_2)); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByForkCountGreaterThanEqualAndCreator(FORK_COUNT_0, CREATOR_0)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + } + + @Test + public void testFindByTrue() { + final List projects = TestUtils.toList(repository.findByHasReleasedTrue()); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_1, PROJECT_2, PROJECT_3)); + } + + @Test + public void testFindByFalse() { + final List projects = TestUtils.toList(repository.findByHasReleasedFalse()); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_4)); + } + + @Test + public void testFindByTrueFalseWithAnd() { + List projects = TestUtils.toList(repository.findByHasReleasedTrueAndCreator(CREATOR_3)); + assertProjectListEquals(projects, Collections.singletonList(PROJECT_3)); + + projects = TestUtils.toList(repository.findByHasReleasedFalseAndCreator(CREATOR_3)); + assertProjectListEquals(projects, Collections.emptyList()); + } + + @Test + public void testFindByTrueFalseWithOr() { + List projects = TestUtils.toList(repository.findByHasReleasedTrueOrCreator(CREATOR_0)); + assertProjectListEquals(projects, PROJECTS); + + projects = TestUtils.toList(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(collectionManager.getEntityInformation(Project.class).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 = TestUtils.toList(repository.findByCreatorIn(Collections.singleton(FAKE_CREATOR))); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByCreatorIn(Arrays.asList(CREATOR_1, CREATOR_2))); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + + projects = TestUtils.toList(repository.findByCreatorIn(Arrays.asList(CREATOR_0, FAKE_CREATOR))); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + + projects = TestUtils.toList(repository.findByCreatorIn(Collections.singletonList(CREATOR_1))); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_1)); + } + + @Test + public void testFindByInWithAnd() { + List projects = TestUtils.toList(repository.findByCreatorInAndStarCountIn(Arrays.asList(CREATOR_0, + CREATOR_1), + Arrays.asList(STAR_COUNT_2, STAR_COUNT_3))); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(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 = TestUtils.toList(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 testFindByInWithOr() { + List projects = TestUtils.toList(repository.findByCreatorInOrStarCount(Arrays.asList(CREATOR_0, + CREATOR_1), STAR_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4, PROJECT_1, PROJECT_2)); + + projects = TestUtils.toList(repository.findByCreatorInOrStarCount(Collections.singletonList(CREATOR_1), + STAR_COUNT_2)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2)); + + projects = TestUtils.toList(repository.findByCreatorInOrStarCount(Collections.singletonList(CREATOR_0), + STAR_COUNT_0)); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_4)); + } + + @Test + public void testFindByNotIn() { + List projects = TestUtils.toList(repository.findByCreatorNotIn( + Arrays.asList(CREATOR_0, CREATOR_1, CREATOR_2, CREATOR_3))); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByCreatorNotIn(Arrays.asList(CREATOR_1, CREATOR_2))); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_0, PROJECT_3, PROJECT_4)); + + projects = TestUtils.toList(repository.findByCreatorNotIn(Arrays.asList(CREATOR_0, FAKE_CREATOR))); + + assertProjectListEquals(projects, Arrays.asList(PROJECT_1, PROJECT_2, PROJECT_3)); + } + + @Test + public void testFindByInWithNotIn() { + List projects = + TestUtils.toList(repository.findByCreatorInAndStarCountNotIn(Collections.singletonList(FAKE_CREATOR), + Arrays.asList(STAR_COUNT_2, STAR_COUNT_3))); + + Assert.assertTrue(projects.isEmpty()); + + projects = TestUtils.toList(repository.findByCreatorInAndStarCountNotIn(Arrays.asList(CREATOR_0, CREATOR_1), + Arrays.asList(STAR_COUNT_0, STAR_COUNT_2))); + + assertProjectListEquals(projects, Collections.singletonList(PROJECT_1)); + + projects = TestUtils.toList(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 = TestUtils.toList(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 = TestUtils.toList(repository.findByNameIsNull()); + + assertProjectListEquals(projects, Collections.singletonList(nullNameProject)); + } + + @Test + public void testFindByNameIsNotNull() { + List projects = TestUtils.toList(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 = TestUtils.toList(repository.findByNameIsNotNull()); + + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindByNameIsNullWithAnd() { + List projects = TestUtils.toList(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 = TestUtils.toList(repository.findByNameIsNullAndForkCount(FORK_COUNT_0)); + + assertProjectListEquals(projects, Collections.singletonList(nullNameProject)); + } + + @Test + public void testFindByNameIsNotNullWithAnd() { + List projects = TestUtils.toList(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 = TestUtils.toList(repository.findByNameIsNotNullAndHasReleased(true)); + Assert.assertTrue(projects.isEmpty()); + } + + @Test + public void testFindAllByPartitionKey() { + List findAll = + TestUtils.toList(repository.findAll(new PartitionKey(CREATOR_0))); + // Since there are two projects with creator_0 + assertThat(findAll.size()).isEqualTo(2); + List projectList = new ArrayList<>(); + projectList.add(PROJECT_0); + projectList.add(PROJECT_4); + assertThat(findAll.containsAll(projectList)).isTrue(); + + findAll = TestUtils.toList(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 = TestUtils.toList(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 = TestUtils.toList(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(); + } + + @Test + public void testSqlInjection() { + List projects = TestUtils.toList(this.repository.findAllByNameIn(Collections.singleton("sql) or (r" + + ".name <> ''"))); + assertTrue(projects.isEmpty()); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ProjectRepositorySortIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ProjectRepositorySortIT.java new file mode 100644 index 000000000000..e3c20e576b1f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ProjectRepositorySortIT.java @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.domain.SortedProject; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.SortedProjectRepository; +import org.assertj.core.util.Lists; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.azure.spring.data.cosmos.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); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private SortedProjectRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, SortedProject.class); + this.repository.saveAll(PROJECTS); + } + + @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 = CosmosAccessException.class) + public void testFindAllSortMoreThanOneOrderException() { + final Sort sort = Sort.by(Sort.Direction.ASC, "name", "creator"); + + this.repository.findAll(sort).iterator().next(); + } + + @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 = CosmosAccessException.class) + public void testFindAllSortMissMatchException() { + final Sort sort = Sort.by(Sort.Direction.ASC, "fake-name"); + + this.repository.findAll(sort).iterator().next(); + } + + 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, 5); + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/QuestionRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/QuestionRepositoryIT.java new file mode 100644 index 000000000000..bbd2ae67dc24 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/QuestionRepositoryIT.java @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Question; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.QuestionRepository; +import org.assertj.core.util.Lists; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private QuestionRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Question.class); + this.repository.save(QUESTION); + } + + @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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveAuditableIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveAuditableIT.java new file mode 100644 index 000000000000..21626c14c2ac --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveAuditableIT.java @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.domain.AuditableEntity; +import com.azure.spring.data.cosmos.domain.AuditableIdGeneratedEntity; +import com.azure.spring.data.cosmos.repository.StubAuditorProvider; +import com.azure.spring.data.cosmos.repository.StubDateTimeProvider; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveAuditableIdGeneratedRepository; +import com.azure.spring.data.cosmos.repository.repository.ReactiveAuditableRepository; +import org.junit.Before; +import org.junit.ClassRule; +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 reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveAuditableIT { + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); + + @Autowired + private ReactiveCosmosTemplate template; + @Autowired + private ReactiveAuditableRepository auditableRepository; + @Autowired + private ReactiveAuditableIdGeneratedRepository reactiveAuditableIdGeneratedRepository; + @Autowired + private StubDateTimeProvider stubDateTimeProvider; + @Autowired + private StubAuditorProvider stubAuditorProvider; + + @Before + public void setup() { + collectionManager.ensureContainersCreatedAndEmpty(template, AuditableEntity.class, AuditableIdGeneratedEntity.class); + } + + @Test + public void testInsertShouldSetAuditableEntries() { + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + final OffsetDateTime now = OffsetDateTime.now(ZoneId.of("UTC")).truncatedTo(ChronoUnit.MICROS); + + stubDateTimeProvider.setNow(now); + stubAuditorProvider.setCurrentAuditor("created-by"); + final Mono savedEntity = auditableRepository.save(entity); + StepVerifier + .create(savedEntity) + .expectNextMatches(actual -> validateAuditableFields(actual, + "created-by", + now, + "created-by", + now)) + .verifyComplete(); + } + + @Test + public void testUpdateShouldNotOverwriteCreatedEntries() { + final AuditableEntity entity = new AuditableEntity(); + entity.setId(UUID.randomUUID().toString()); + final OffsetDateTime createdOn = OffsetDateTime.now(ZoneId.of("UTC")).truncatedTo(ChronoUnit.MICROS); + + stubDateTimeProvider.setNow(createdOn); + stubAuditorProvider.setCurrentAuditor("created-by"); + final Mono subscribe = auditableRepository.save(entity); + final AuditableEntity saved = subscribe.block(); + + final OffsetDateTime modifiedOn = createdOn.plusMinutes(1); + stubDateTimeProvider.setNow(modifiedOn); + stubAuditorProvider.setCurrentAuditor("modified-by"); + final Mono modified = auditableRepository.save(saved); + StepVerifier + .create(modified) + .expectNextMatches(actual -> validateAuditableFields(actual, + "created-by", + createdOn, + "modified-by", + modifiedOn)) + .verifyComplete(); + } + + private boolean validateAuditableFields(AuditableEntity entity, + String expectedCreatedBy, OffsetDateTime expectedCreatedDate, + String expectedModifiedBy, OffsetDateTime expectedModifiedTime) { + assertThat(entity.getCreatedBy()).isEqualTo(expectedCreatedBy); + assertThat(entity.getCreatedDate()).isEqualTo(expectedCreatedDate); + assertThat(entity.getLastModifiedBy()).isEqualTo(expectedModifiedBy); + assertThat(entity.getLastModifiedByDate()).isEqualTo(expectedModifiedTime); + return true; + } + + @Test + public void testInsertShouldSetAuditableEntriesIfIdAutoGenerated() { + final AuditableIdGeneratedEntity entity = new AuditableIdGeneratedEntity(); + final OffsetDateTime now = OffsetDateTime.now(ZoneId.of("UTC")).truncatedTo(ChronoUnit.MICROS); + + stubDateTimeProvider.setNow(now); + stubAuditorProvider.setCurrentAuditor("created-by"); + final Mono savedEntity = reactiveAuditableIdGeneratedRepository.save(entity); + + StepVerifier + .create(savedEntity) + .expectNextMatches(actual -> validateAuditableFields(actual, + "created-by", now, + "created-by", now)) + .verifyComplete(); + } + + private boolean validateAuditableFields(AuditableIdGeneratedEntity entity, + String expectedCreatedBy, OffsetDateTime expectedCreatedDate, + String expectedModifiedBy, OffsetDateTime expectedModifiedTime) { + assertThat(entity.getCreatedBy()).isEqualTo(expectedCreatedBy); + assertThat(entity.getCreatedDate()).isEqualTo(expectedCreatedDate); + assertThat(entity.getLastModifiedBy()).isEqualTo(expectedModifiedBy); + assertThat(entity.getLastModifiedByDate()).isEqualTo(expectedModifiedTime); + return true; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java new file mode 100644 index 000000000000..dca4f14667bb --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.implementation.PreconditionFailedException; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.domain.Course; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveCourseRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.assertj.core.api.Assertions; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.HashSet; +import java.util.Set; +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 PATCH_COURSE_NAME_1 = "PathedCourse1"; + + 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); + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); + + @Autowired + private ReactiveCosmosTemplate template; + + @Autowired + private ReactiveCourseRepository repository; + private CosmosEntityInformation entityInformation; + + CosmosPatchOperations patchSetOperation = CosmosPatchOperations + .create() + .set("/name", PATCH_COURSE_NAME_1); + + CosmosPatchOperations patchReplaceOperation = CosmosPatchOperations + .create() + .replace("/name", PATCH_COURSE_NAME_1); + + CosmosPatchOperations patchRemoveOperation = CosmosPatchOperations + .create() + .remove("/name"); + + private static final CosmosPatchItemRequestOptions options = new CosmosPatchItemRequestOptions(); + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Course.class); + entityInformation = collectionManager.getEntityInformation(Course.class); + final Flux savedFlux = repository.saveAll(Arrays.asList(COURSE_1, COURSE_2, + COURSE_3, COURSE_4)); + StepVerifier.create(savedFlux).thenConsumeWhile(course -> true).expectComplete().verify(); + } + + @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 testFindOneShouldFailIfMultipleResultsReturned() { + final Course course = new Course("unusedId", COURSE_1.getName(), COURSE_1.getDepartment()); + final Mono saveSecond = repository.save(course); + StepVerifier.create(saveSecond).expectNext(course).verifyComplete(); + + final Mono find = repository.findOneByName(COURSE_1.getName()); + StepVerifier.create(find).expectError(CosmosAccessException.class).verify(); + } + + @Test + public void testShouldFindSingleEntity() { + final Mono find = repository.findOneByName(COURSE_1.getName()); + StepVerifier.create(find).expectNext(COURSE_1).expectComplete().verify(); + } + + @Test + public void testShouldReturnEmptyMonoWhenNoResults() { + final Mono find = repository.findOneByName("unusedName"); + StepVerifier.create(find).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(CosmosAccessException.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(CosmosAccessException.class).verify(); + } + + @Test + public void testDeleteByEntityNotFound() { + final Mono deleteMono = repository.delete(COURSE_5); + StepVerifier.create(deleteMono).expectError(CosmosAccessException.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(); + } + + @Test + public void testFindByNameIgnoreCase() { + final Flux findResult = repository.findByNameIgnoreCase(COURSE_NAME_1.toLowerCase()); + StepVerifier.create(findResult).expectNext(COURSE_1).verifyComplete(); + } + + @Test + public void testFindByNameAndDepartmentAllIgnoreCase() { + final Flux findResult = repository.findByNameAndDepartmentAllIgnoreCase( + COURSE_NAME_1.toLowerCase(), DEPARTMENT_NAME_3.toLowerCase()); + StepVerifier.create(findResult).expectNext(COURSE_1).verifyComplete(); + } + + @Test + public void testFindByNameAndDepartmentOrNameAndDepartment() { + final Flux findResult = repository.findByNameAndDepartmentOrNameAndDepartment( + COURSE_NAME_1, DEPARTMENT_NAME_3, COURSE_NAME_2, DEPARTMENT_NAME_2); + final Set courseResultSet = new HashSet<>(); + courseResultSet.add(COURSE_1); + courseResultSet.add(COURSE_2); + StepVerifier.create(findResult).expectNextCount(2).thenConsumeWhile(value -> { + Assertions.assertThat(courseResultSet.contains(value)).isTrue(); + return true; + }).verifyComplete(); + } + + @Test + public void testFindByNameOrDepartmentAllIgnoreCase() { + final Flux findResult = repository.findByNameOrDepartmentAllIgnoreCase( + COURSE_NAME_1.toLowerCase(), DEPARTMENT_NAME_3.toLowerCase()); + StepVerifier.create(findResult).expectNext(COURSE_1).verifyComplete(); + } + + @Test + public void testFindByNameJsonNode() { + final Flux findResult = repository.annotatedFindByName(COURSE_NAME_1); + StepVerifier.create(findResult).consumeNextWith(result -> { + Assert.assertEquals(result.findValue("courseId").asText(), COURSE_1.getCourseId()); + Assert.assertEquals(result.findValue("name").asText(), COURSE_1.getName()); + Assert.assertEquals(result.findValue("department").asText(), COURSE_1.getDepartment()); + }).verifyComplete(); + } + + @Test + public void testAnnotatedQueries() { + Flux courseFlux = repository.getCoursesWithNameDepartment(COURSE_NAME_1, DEPARTMENT_NAME_3); + StepVerifier.create(courseFlux).expectNext(COURSE_1).verifyComplete(); + + Flux courseGroupBy = repository.getCoursesGroupByDepartment(); + StepVerifier.create(courseGroupBy).expectComplete(); + StepVerifier.create(courseGroupBy).expectNextCount(1); + } + + @Test + public void testPatchEntitySet() { + Mono patch = repository.save(COURSE_ID_1, new PartitionKey(DEPARTMENT_NAME_3), Course.class, patchSetOperation); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Mono patchedCourse = repository.findById(COURSE_ID_1); + StepVerifier.create(patchedCourse).expectNextMatches(course -> course.getName().equals(PATCH_COURSE_NAME_1)).verifyComplete(); + } + + @Test + public void testPatchEntityReplace() { + Mono patch = repository.save(COURSE_ID_2, new PartitionKey(DEPARTMENT_NAME_2), Course.class, patchReplaceOperation); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Mono patchedCourse = repository.findById(COURSE_ID_2); + StepVerifier.create(patchedCourse).expectNextMatches(course -> course.getName().equals(PATCH_COURSE_NAME_1)).verifyComplete(); + } + + @Test + public void testPatchEntityRemove() { + Mono patch = repository.save(COURSE_ID_1, new PartitionKey(DEPARTMENT_NAME_3), Course.class, patchRemoveOperation); + StepVerifier.create(patch).expectNextCount(1).verifyComplete(); + Mono patchedCourse = repository.findById(COURSE_ID_1); + StepVerifier.create(patchedCourse).expectNextMatches(course -> course.getName() == null).verifyComplete(); + } + @Test + public void testPatchPreConditionSuccess() { + options.setFilterPredicate("FROM course a WHERE a.department = '"+DEPARTMENT_NAME_3+"'"); + Mono patchedCourse = repository.save(COURSE_ID_1, new PartitionKey(DEPARTMENT_NAME_3), Course.class, patchSetOperation, options); + StepVerifier.create(patchedCourse).expectNextMatches(course -> course.getName().equals(PATCH_COURSE_NAME_1)).verifyComplete(); + } + + @Test + public void testPatchPreConditionFail() { + options.setFilterPredicate("FROM course a WHERE a.department = 'dummy'"); + Mono patchedCourse = repository.save(COURSE_ID_1, new PartitionKey(DEPARTMENT_NAME_3), Course.class, patchSetOperation, options); + StepVerifier.create(patchedCourse).expectErrorMatches(ex -> ex instanceof CosmosAccessException && + ((CosmosAccessException) ex).getCosmosException() instanceof PreconditionFailedException).verify(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveEtagIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveEtagIT.java new file mode 100644 index 000000000000..d6c3b2dc3f32 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveEtagIT.java @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.domain.CourseWithEtag; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveCourseWithEtagRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +import static com.azure.spring.data.cosmos.common.TestConstants.COURSE_NAME; +import static com.azure.spring.data.cosmos.common.TestConstants.DEPARTMENT; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveEtagIT { + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); + + @Autowired + ReactiveCosmosTemplate template; + @Autowired + ReactiveCourseWithEtagRepository reactiveCourseWithEtagRepository; + + @Before + public void setup() { + collectionManager.ensureContainersCreatedAndEmpty(template, CourseWithEtag.class); + } + + private static CourseWithEtag createCourseWithEtag() { + return new CourseWithEtag(UUID.randomUUID().toString(), COURSE_NAME, DEPARTMENT); + } + + @Test + public void testCrudOperationsShouldApplyEtag() { + final Mono insertedCourseWithEtagMono = + reactiveCourseWithEtagRepository.save(createCourseWithEtag()); + CourseWithEtag insertedCourseWithEtag = insertedCourseWithEtagMono.block(); + Assert.assertNotNull(insertedCourseWithEtag); + Assert.assertNotNull(insertedCourseWithEtag.getEtag()); + + insertedCourseWithEtag.setName("CHANGED"); + final Mono updatedCourseWithEtagMono = + reactiveCourseWithEtagRepository.save(insertedCourseWithEtag); + CourseWithEtag updatedCourseWithEtag = updatedCourseWithEtagMono.block(); + Assert.assertNotNull(updatedCourseWithEtag); + Assert.assertNotNull(updatedCourseWithEtag.getEtag()); + Assert.assertNotEquals(updatedCourseWithEtag.getEtag(), insertedCourseWithEtag.getEtag()); + + final Mono foundCourseWithEtagMono = + reactiveCourseWithEtagRepository.findById(insertedCourseWithEtag.getCourseId()); + CourseWithEtag foundCourseWithEtag = foundCourseWithEtagMono.block(); + Assert.assertNotNull(foundCourseWithEtag); + Assert.assertNotNull(foundCourseWithEtag.getEtag()); + Assert.assertEquals(foundCourseWithEtag.getEtag(), updatedCourseWithEtag.getEtag()); + } + + @Test + public void testCrudListOperationsShouldApplyEtag() { + final List courses = new ArrayList<>(); + courses.add(createCourseWithEtag()); + courses.add(createCourseWithEtag()); + + final Flux insertedCourseWithEtagsFlux = reactiveCourseWithEtagRepository.saveAll(courses); + List insertedCourseWithEtags = insertedCourseWithEtagsFlux.collectList().block(); + + Assert.assertNotNull(insertedCourseWithEtags); + insertedCourseWithEtags.forEach(course -> Assert.assertNotNull(course.getEtag())); + + insertedCourseWithEtags.forEach(course -> course.setName("CHANGED")); + final Flux updatedCourseWithEtagsFlux = + reactiveCourseWithEtagRepository.saveAll(insertedCourseWithEtags); + List updatedCourseWithEtags = updatedCourseWithEtagsFlux.collectList().block(); + + Assert.assertNotNull(updatedCourseWithEtags); + + insertedCourseWithEtags.sort(Comparator.comparing(CourseWithEtag::getCourseId)); + updatedCourseWithEtags.sort(Comparator.comparing(CourseWithEtag::getCourseId)); + + for (int i = 0; i < updatedCourseWithEtags.size(); i++) { + CourseWithEtag insertedCourseWithEtag = insertedCourseWithEtags.get(i); + CourseWithEtag updatedCourseWithEtag = updatedCourseWithEtags.get(i); + Assert.assertEquals(insertedCourseWithEtag.getCourseId(), updatedCourseWithEtag.getCourseId()); + Assert.assertNotNull(updatedCourseWithEtag.getEtag()); + Assert.assertNotEquals(insertedCourseWithEtag.getEtag(), updatedCourseWithEtag.getEtag()); + } + } + + @Test + public void testShouldFailIfEtagDoesNotMatch() { + Mono insertedCourseWithEtagMono = reactiveCourseWithEtagRepository.save(createCourseWithEtag()); + CourseWithEtag insertedCourseWithEtag = insertedCourseWithEtagMono.block(); + Assert.assertNotNull(insertedCourseWithEtag); + + insertedCourseWithEtag.setName("CHANGED"); + Mono updatedCourseWithEtagMono = reactiveCourseWithEtagRepository.save(insertedCourseWithEtag); + CourseWithEtag updatedCourseWithEtag = updatedCourseWithEtagMono.block(); + Assert.assertNotNull(updatedCourseWithEtag); + updatedCourseWithEtag.setEtag(insertedCourseWithEtag.getEtag()); + + + Mono courseMono = reactiveCourseWithEtagRepository.save(updatedCourseWithEtag); + StepVerifier.create(courseMono).verifyError(CosmosAccessException.class); + + Mono deleteMono = reactiveCourseWithEtagRepository.delete(updatedCourseWithEtag); + StepVerifier.create(deleteMono).verifyError(CosmosAccessException.class); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveIndexPolicyUpdateIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveIndexPolicyUpdateIT.java new file mode 100644 index 000000000000..37b577751f29 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveIndexPolicyUpdateIT.java @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.implementation.ImplementationBridgeHelpers; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.ExcludedPath; +import com.azure.cosmos.models.IncludedPath; +import com.azure.cosmos.models.IndexingMode; +import com.azure.cosmos.models.IndexingPolicy; +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.domain.ComplexIndexPolicyEntity; +import com.azure.spring.data.cosmos.domain.IndexPolicyEntity; +import com.azure.spring.data.cosmos.domain.IndexPolicyOverwriteEntity; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.azure.spring.data.cosmos.repository.support.SimpleReactiveCosmosRepository; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveIndexPolicyUpdateIT { + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); + + @Autowired + ReactiveCosmosTemplate template; + + @Autowired + ApplicationContext context; + + CosmosEntityInformation defaultIndexPolicyEntityInformation = new CosmosEntityInformation<>(IndexPolicyEntity.class); + + CosmosEntityInformation complexIndexPolicyEntityInformation = new CosmosEntityInformation<>(ComplexIndexPolicyEntity.class); + + CosmosEntityInformation indexPolicyOverwriteEntityInformation = new CosmosEntityInformation<>(IndexPolicyOverwriteEntity.class); + + CosmosEntityInformation addressEntityInformation = new CosmosEntityInformation<>(Address.class); + + @Before + public void setup() { + collectionManager.ensureContainersCreatedAndEmpty(template, IndexPolicyEntity.class, ComplexIndexPolicyEntity.class, IndexPolicyOverwriteEntity.class); + } + + @Test + public void testIndexPolicyDoesntUpdateOnRepoInitialization() { + // set index policy based on entity annotation + collectionManager.deleteContainer(defaultIndexPolicyEntityInformation); + new SimpleReactiveCosmosRepository<>(defaultIndexPolicyEntityInformation, template); + + // get original index policy + CosmosContainerProperties properties = template.getContainerProperties(defaultIndexPolicyEntityInformation.getContainerName()).block(); + + // assert + assertThat(properties.getIndexingPolicy().getIncludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getIncludedPaths().get(0).getPath()).isEqualTo("/*"); + assertThat(properties.getIndexingPolicy().getExcludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getExcludedPaths().get(0).getPath()).isEqualTo("/\"_etag\"/?"); + assertThat(properties.getIndexingPolicy().isAutomatic()).isEqualTo(true); + assertThat(properties.getIndexingPolicy().getIndexingMode()).isEqualTo(IndexingMode.CONSISTENT); + + // set new index policy + IndexingPolicy newIndexPolicy = new IndexingPolicy(); + newIndexPolicy.setIncludedPaths(Collections.singletonList(new IncludedPath("/field/?"))); + newIndexPolicy.setExcludedPaths(Collections.singletonList(new ExcludedPath("/*"))); + + // apply new index policy + CosmosEntityInformation spyEntityInformation = Mockito.spy(defaultIndexPolicyEntityInformation); + Mockito.doReturn(newIndexPolicy).when(spyEntityInformation).getIndexingPolicy(); + Mockito.doReturn(false).when(spyEntityInformation).isOverwriteIndexingPolicy(); + new SimpleReactiveCosmosRepository<>(spyEntityInformation, template); + + // retrieve updated index policy + properties = template.getContainerProperties(defaultIndexPolicyEntityInformation.getContainerName()).block(); + + // assert + assertThat(properties.getIndexingPolicy().getIncludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getIncludedPaths().get(0).getPath()).isEqualTo("/*"); + assertThat(properties.getIndexingPolicy().getExcludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getExcludedPaths().get(0).getPath()).isEqualTo("/\"_etag\"/?"); + assertThat(properties.getIndexingPolicy().isAutomatic()).isEqualTo(true); + assertThat(properties.getIndexingPolicy().getIndexingMode()).isEqualTo(IndexingMode.CONSISTENT); + } + + @Test + public void testIndexPolicyUpdatesOnRepoInitialization() { + // set index policy from entity annotation + collectionManager.deleteContainer(indexPolicyOverwriteEntityInformation); + new SimpleReactiveCosmosRepository<>(indexPolicyOverwriteEntityInformation, template); + + // get original index policy + CosmosContainerProperties properties = template.getContainerProperties(indexPolicyOverwriteEntityInformation.getContainerName()).block(); + + // assert + assertThat(properties.getIndexingPolicy().getIncludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getIncludedPaths().get(0).getPath()).isEqualTo("/\"_etag\"/?"); + assertThat(properties.getIndexingPolicy().getExcludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getExcludedPaths().get(0).getPath()).isEqualTo("/*"); + + // set new index policy + IndexingPolicy newIndexPolicy = new IndexingPolicy(); + newIndexPolicy.setIncludedPaths(Collections.singletonList(new IncludedPath("/*"))); + newIndexPolicy.setExcludedPaths(Collections.singletonList(new ExcludedPath("/\"_etag\"/?"))); + + // apply new index policy + CosmosEntityInformation spyEntityInformation = Mockito.spy(indexPolicyOverwriteEntityInformation); + Mockito.doReturn(newIndexPolicy).when(spyEntityInformation).getIndexingPolicy(); + Mockito.doReturn(true).when(spyEntityInformation).isOverwriteIndexingPolicy(); + new SimpleReactiveCosmosRepository<>(spyEntityInformation, template); + + // retrieve updated index policy + properties = template.getContainerProperties(indexPolicyOverwriteEntityInformation.getContainerName()).block(); + + // assert + assertThat(properties.getIndexingPolicy().getIncludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getIncludedPaths().get(0).getPath()).isEqualTo("/*"); + assertThat(properties.getIndexingPolicy().getExcludedPaths().size()).isEqualTo(1); + assertThat(properties.getIndexingPolicy().getExcludedPaths().get(0).getPath()).isEqualTo("/\"_etag\"/?"); + } + + @Test + public void testContainerReplaceShouldNotOccurIfIndexIsUnchanged() { + new SimpleReactiveCosmosRepository<>(defaultIndexPolicyEntityInformation, template); + ReactiveCosmosTemplate spyTemplate = Mockito.spy(template); + new SimpleReactiveCosmosRepository<>(defaultIndexPolicyEntityInformation, spyTemplate); + Mockito.verify(spyTemplate, Mockito.never()).replaceContainerProperties(Mockito.any(), Mockito.any()); + } + + @Test + public void testContainerReplaceShouldNotOccurIfComplexIndexIsUnchanged() { + new SimpleReactiveCosmosRepository<>(complexIndexPolicyEntityInformation, template); + ReactiveCosmosTemplate spyTemplate = Mockito.spy(template); + new SimpleReactiveCosmosRepository<>(complexIndexPolicyEntityInformation, spyTemplate); + Mockito.verify(spyTemplate, Mockito.never()).replaceContainerProperties(Mockito.any(), Mockito.any()); + } + + @Test + public void testContainerReplaceShouldNotOccurIfIndexingPolicyIsNotSpecified() { + new SimpleReactiveCosmosRepository<>(addressEntityInformation, template); + ReactiveCosmosTemplate spyTemplate = Mockito.spy(template); + new SimpleReactiveCosmosRepository<>(addressEntityInformation, spyTemplate); + Mockito.verify(spyTemplate, Mockito.never()).replaceContainerProperties(Mockito.any(), Mockito.any()); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveLongIdDomainPartitionPartitionRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveLongIdDomainPartitionPartitionRepositoryIT.java new file mode 100644 index 000000000000..823b7897bc8f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveLongIdDomainPartitionPartitionRepositoryIT.java @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.domain.LongIdDomainPartition; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveLongIdDomainPartitionRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Before; +import org.junit.ClassRule; +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.Objects; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveLongIdDomainPartitionPartitionRepositoryIT { + + private static final Long ID_1 = 12345L; + private static final String NAME_1 = "moary"; + + private static final Long ID_2 = 67890L; + private static final String NAME_2 = "camille"; + + private static final LongIdDomainPartition DOMAIN_1 = new LongIdDomainPartition(ID_1, NAME_1); + private static final LongIdDomainPartition DOMAIN_2 = new LongIdDomainPartition(ID_2, NAME_2); + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); + + @Autowired + private ReactiveCosmosTemplate template; + + @Autowired + private ReactiveLongIdDomainPartitionRepository repository; + + private CosmosEntityInformation entityInformation; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, LongIdDomainPartition.class); + entityInformation = collectionManager.getEntityInformation(LongIdDomainPartition.class); + Flux savedAllFlux = this.repository.saveAll(Arrays.asList(DOMAIN_1, DOMAIN_2)); + StepVerifier.create(savedAllFlux).thenConsumeWhile(domain -> true).expectComplete().verify(); + } + + @Test + public void testLongIdDomainPartition() { + Mono deletedMono = this.repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + Mono idMono = this.repository.findById(ID_1, + new PartitionKey(entityInformation.getPartitionKeyFieldValue(DOMAIN_1))); + StepVerifier.create(idMono).expectNextCount(0).verifyComplete(); + + Mono saveMono = this.repository.save(DOMAIN_1); + StepVerifier.create(saveMono).expectNext(DOMAIN_1).expectComplete().verify(); + + Mono findIdMono = this.repository.findById(ID_1, + new PartitionKey(entityInformation.getPartitionKeyFieldValue(DOMAIN_1))); + StepVerifier.create(findIdMono).expectNext(DOMAIN_1).expectComplete().verify(); + + Mono deleteMono = this.repository.delete(DOMAIN_1); + StepVerifier.create(deleteMono).verifyComplete(); + + Mono afterDelIdMono = this.repository.findById(ID_1, + new PartitionKey(entityInformation.getPartitionKeyFieldValue(DOMAIN_1))); + StepVerifier.create(afterDelIdMono).expectNextCount(0).verifyComplete(); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidDomain() { + new CosmosEntityInformation(InvalidDomain.class); + } + + @Test + public void testSaveAllAndFindAll() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + Flux savedAllFlux = this.repository.saveAll(Arrays.asList(DOMAIN_1, DOMAIN_2)); + StepVerifier.create(savedAllFlux).expectNextCount(2).verifyComplete(); + + final Flux allFlux = repository.findAll(); + StepVerifier.create(allFlux).expectNextCount(2).verifyComplete(); + } + + @Test + public void testCount() { + Mono countMono = repository.count(); + StepVerifier.create(countMono).expectNext(2L).verifyComplete(); + } + + @Test + public void testDeleteByIdWithoutPartitionKey() { + final Mono deleteMono = repository.deleteById(DOMAIN_1.getNumber()); + StepVerifier.create(deleteMono).expectError(CosmosAccessException.class).verify(); + } + + @Test + public void testDeleteByIdAndPartitionKey() { + final Mono deleteMono = repository.deleteById(DOMAIN_1.getNumber(), + new PartitionKey(entityInformation.getPartitionKeyFieldValue(DOMAIN_1))); + StepVerifier.create(deleteMono).verifyComplete(); + + Mono findIdMono = this.repository.findById(ID_1, + new PartitionKey(entityInformation.getPartitionKeyFieldValue(DOMAIN_1))); + StepVerifier.create(findIdMono).expectNextCount(0).verifyComplete(); + } + + @Test + public void testDeleteByIdShouldFailIfNothingToDelete() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + final Mono deleteIdMono = repository.deleteById(DOMAIN_1.getNumber(), + new PartitionKey(entityInformation.getPartitionKeyFieldValue(DOMAIN_1))); + StepVerifier.create(deleteIdMono).expectError(CosmosAccessException.class).verify(); + } + + @Test + public void testDelete() { + Mono saveMono = this.repository.save(DOMAIN_1); + StepVerifier.create(saveMono).expectNext(DOMAIN_1).expectComplete().verify(); + + Mono deleteMono = this.repository.delete(DOMAIN_1); + StepVerifier.create(deleteMono).verifyComplete(); + + Mono countMono = repository.count(); + StepVerifier.create(countMono).expectNext(1L).verifyComplete(); + } + + @Test + public void testDeleteShouldFailIfNothingToDelete() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + Mono deleteIdMono = this.repository.delete(DOMAIN_1); + StepVerifier.create(deleteIdMono).expectError(CosmosAccessException.class).verify(); + } + + @Test + public void testDeleteAll() { + Flux savedAllFlux = this.repository.saveAll(Arrays.asList(DOMAIN_1, DOMAIN_2)); + StepVerifier.create(savedAllFlux).expectNextCount(2).verifyComplete(); + + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + + Mono countMono = repository.count(); + StepVerifier.create(countMono).expectNext(0L).verifyComplete(); + } + + @Test + public void testExistsById() { + Mono saveMono = this.repository.save(DOMAIN_1); + StepVerifier.create(saveMono).expectNext(DOMAIN_1).expectComplete().verify(); + + Mono booleanMono = this.repository.existsById(DOMAIN_1.getNumber()); + StepVerifier.create(booleanMono).expectNext(true).expectComplete().verify(); + } + + @Test + public void testFindAllSort() { + final LongIdDomainPartition other = new LongIdDomainPartition( + DOMAIN_1.getNumber() + 1, "other-name"); + Flux savedAllFlux = this.repository.saveAll(Arrays.asList(DOMAIN_1, other)); + StepVerifier.create(savedAllFlux).thenConsumeWhile(domain -> true).expectComplete().verify(); + + final Sort ascSort = Sort.by(Sort.Direction.ASC, "number"); + Flux ascAllFlux = this.repository.findAll(ascSort); + StepVerifier.create(ascAllFlux).expectNext(DOMAIN_1, other, DOMAIN_2).verifyComplete(); + + final Sort descSort = Sort.by(Sort.Direction.DESC, "number"); + Flux descAllFlux = this.repository.findAll(descSort); + StepVerifier.create(descAllFlux).expectNext(DOMAIN_2, other, DOMAIN_1).verifyComplete(); + } + + private static class InvalidDomain { + + private long count; + + private String location; + + InvalidDomain() { + } + + InvalidDomain(long count, String location) { + this.count = count; + this.location = location; + } + + public long getCount() { + return count; + } + + public void setCount(long 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveNestedPartitionKeyRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveNestedPartitionKeyRepositoryIT.java new file mode 100644 index 000000000000..fb583422fe3a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveNestedPartitionKeyRepositoryIT.java @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.NestedEntity; +import com.azure.spring.data.cosmos.domain.NestedPartitionKeyEntity; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveNestedPartitionKeyRepository; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveNestedPartitionKeyRepositoryIT { + + private static final NestedPartitionKeyEntity NESTED_ENTITY_1 = + new NestedPartitionKeyEntity(null, new NestedEntity("partitionKey1")); + + private static final NestedPartitionKeyEntity NESTED_ENTITY_2 = + new NestedPartitionKeyEntity(null, new NestedEntity("partitionKey2")); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + ReactiveNestedPartitionKeyRepository repository; + + @BeforeClass + public static void init() { } + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, NestedPartitionKeyEntity.class); + } + + @Test + public void testSave() { + Mono savedEntity = repository.save(NESTED_ENTITY_1); + + StepVerifier.create(savedEntity).expectNext(NESTED_ENTITY_1).verifyComplete(); + } + + @Test + public void testFindAll() { + Flux nestedPartitionKeyEntityFlux = + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + StepVerifier.create(nestedPartitionKeyEntityFlux).expectNextCount(2).verifyComplete(); + + Flux iterable = repository.findAll(); + StepVerifier.create(iterable).expectNextCount(2).verifyComplete(); + } + + @Test + public void testFindAllByPartitionKey() { + Flux nestedPartitionKeyEntityFlux = + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + StepVerifier.create(nestedPartitionKeyEntityFlux).expectNextCount(2).verifyComplete(); + + Flux fluxEntities = repository.findAll(new PartitionKey("partitionKey2")); + StepVerifier.create(fluxEntities).expectNext(NESTED_ENTITY_2).verifyComplete(); + } + + @Test + public void testFindByIdAndPartitionKey() { + Flux nestedPartitionKeyEntityFlux = + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + StepVerifier.create(nestedPartitionKeyEntityFlux).expectNextCount(2).verifyComplete(); + + Mono nestedEntityMono = repository.findById(NESTED_ENTITY_1.getId(), + new PartitionKey(NESTED_ENTITY_1.getNestedEntity().getNestedPartitionKey())); + StepVerifier.create(nestedEntityMono).expectNext(NESTED_ENTITY_1).verifyComplete(); + } + + @Test + public void testDeleteByIdAndPartitionKey() { + Flux nestedPartitionKeyEntityFlux = + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + StepVerifier.create(nestedPartitionKeyEntityFlux).expectNextCount(2).verifyComplete(); + + Mono voidMono = repository.deleteById(NESTED_ENTITY_1.getId(), + new PartitionKey(NESTED_ENTITY_1.getNestedEntity().getNestedPartitionKey())); + StepVerifier.create(voidMono).verifyComplete(); + Flux findAllFlux = repository.findAll(); + + StepVerifier.create(findAllFlux).expectNextCount(1).verifyComplete(); + } + + @Test + public void testDeleteByEntity() { + Flux nestedPartitionKeyEntityFlux = + repository.saveAll(Arrays.asList(NESTED_ENTITY_1, NESTED_ENTITY_2)); + + StepVerifier.create(nestedPartitionKeyEntityFlux).expectNextCount(2).verifyComplete(); + + Mono delete = repository.delete(NESTED_ENTITY_1); + StepVerifier.create(delete).verifyComplete(); + Flux findFlux = repository.findAll(); + StepVerifier.create(findFlux).expectNext(NESTED_ENTITY_2).verifyComplete(); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveRoleRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveRoleRepositoryIT.java new file mode 100644 index 000000000000..da91c14768b0 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveRoleRepositoryIT.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Role; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveRoleRepository; +import org.junit.Before; +import org.junit.ClassRule; +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.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveRoleRepositoryIT { + + private static final Role TEST_ROLE_1 = new Role(TestConstants.ID_1, true, + TestConstants.ROLE_NAME, TestConstants.LEVEL); + private static final Role TEST_ROLE_2 = new Role(TestConstants.ID_2, false, + TestConstants.ROLE_NAME, TestConstants.LEVEL); + private static final Role TEST_ROLE_3 = new Role(TestConstants.ID_3, true, + TestConstants.ROLE_NAME, TestConstants.LEVEL); + private static final Role TEST_ROLE_4 = new Role(TestConstants.ID_4, true, + TestConstants.ROLE_NAME, TestConstants.LEVEL_2); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + @Autowired + private ReactiveRoleRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Role.class); + final Flux savedFlux = repository.saveAll(Arrays.asList(TEST_ROLE_1, TEST_ROLE_2, TEST_ROLE_3, TEST_ROLE_4)); + StepVerifier.create(savedFlux).thenConsumeWhile(role -> true).expectComplete().verify(); + } + + @Test + public void testAnnotatedQuery() { + Flux roleFlux = repository.annotatedFindRoleById(TestConstants.ID_1); + StepVerifier.create(roleFlux).expectNext(TEST_ROLE_1).verifyComplete(); + } + + @Test + public void testAnnotatedQueryWithSort() { + final Flux roleAscFlux = repository.annotatedFindDeveloperByLevel(TestConstants.LEVEL, Sort.by(Sort.Direction.ASC, "id")); + StepVerifier.create(roleAscFlux) + .expectNext(TEST_ROLE_1) + .expectNext(TEST_ROLE_3) + .verifyComplete(); + + final Flux roleDescFlux = repository.annotatedFindDeveloperByLevel(TestConstants.LEVEL, Sort.by(Sort.Direction.DESC, "id")); + StepVerifier.create(roleDescFlux) + .expectNext(TEST_ROLE_3) + .expectNext(TEST_ROLE_1) + .verifyComplete(); + } + + @Test + public void testAnnotatedQueryWithMultipleLevels() { + List levels = new ArrayList<>(); + levels.add(TestConstants.LEVEL); + final Flux roleAscFlux = repository.annotatedFindRoleByLevelIn(levels, Sort.by(Sort.Direction.ASC, "id")); + StepVerifier.create(roleAscFlux) + .expectNext(TEST_ROLE_1) + .expectNext(TEST_ROLE_2) + .expectNext(TEST_ROLE_3) + .verifyComplete(); + + List levels2 = new ArrayList<>(); + levels2.add(TestConstants.LEVEL); + levels2.add(TestConstants.LEVEL_2); + final Flux roleAscFlux2 = repository.annotatedFindRoleByLevelIn(levels2, Sort.by(Sort.Direction.ASC, "id")); + StepVerifier.create(roleAscFlux2) + .expectNext(TEST_ROLE_1) + .expectNext(TEST_ROLE_2) + .expectNext(TEST_ROLE_3) + .expectNext(TEST_ROLE_4) + .verifyComplete(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveTeacherRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveTeacherRepositoryIT.java new file mode 100644 index 000000000000..f9cd828a8aff --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveTeacherRepositoryIT.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.ReactiveIntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.domain.ReactiveTeacher; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.ReactiveTeacherRepository; +import org.junit.Before; +import org.junit.ClassRule; +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.ArrayList; +import java.util.Arrays; +import java.util.List; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class ReactiveTeacherRepositoryIT { + + private static final String TEACHER_ID_1 = "1"; + + private static final String TEACHER_ID_2 = "2"; + + private static final String TEACHER_ID_3 = "3"; + + private static final String TEACHER_FIRST_NAME_1 = "FirstName1"; + + private static final String TEACHER_FIRST_NAME_2 = "FirstName2"; + + private static final String DEPARTMENT_LAST_NAME_1 = "LastName1"; + + private static final String DEPARTMENT_LAST_NAME_2 = "LastName2"; + + private static final ReactiveTeacher TEACHER_1 = new ReactiveTeacher(TEACHER_ID_1, TEACHER_FIRST_NAME_1, DEPARTMENT_LAST_NAME_1); + + private static final ReactiveTeacher TEACHER_2 = new ReactiveTeacher(TEACHER_ID_2, TEACHER_FIRST_NAME_1, DEPARTMENT_LAST_NAME_2); + + private static final ReactiveTeacher TEACHER_3 = new ReactiveTeacher(TEACHER_ID_3, TEACHER_FIRST_NAME_2, DEPARTMENT_LAST_NAME_1); + + @ClassRule + public static final ReactiveIntegrationTestCollectionManager collectionManager = new ReactiveIntegrationTestCollectionManager(); + + @Autowired + private ReactiveCosmosTemplate template; + + @Autowired + private ReactiveTeacherRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, ReactiveTeacher.class); + final Flux savedFlux = repository.saveAll(Arrays.asList(TEACHER_1)); + StepVerifier.create(savedFlux).thenConsumeWhile(ReactiveTeacher -> true).expectComplete().verify(); + } + + @Test + public void testSaveWithSuppressedNullValue() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + String teacherId = TEACHER_ID_1 + "-Other"; + final ReactiveTeacher teacher = new ReactiveTeacher(teacherId, TEACHER_FIRST_NAME_1, null); + final Mono saveSecond = repository.save(teacher); + StepVerifier.create(saveSecond).expectNext(teacher).verifyComplete(); + + final Mono idMono = repository.findById(teacherId); + StepVerifier.create(idMono).expectNext(teacher).expectComplete().verify(); + + final Mono existFirstNameMono = repository.existsByFirstNameIsNotNull(); + StepVerifier.create(existFirstNameMono).expectNext(true).expectComplete().verify(); + + final Mono existLastNameMono = repository.existsByLastNameIsNull(); + StepVerifier.create(existLastNameMono).expectNext(false).expectComplete().verify(); + } + + @Test + public void testAnnotatedQueryWithArrayContains() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + final Flux savedFlux = repository.saveAll(Arrays.asList(TEACHER_1, TEACHER_2, TEACHER_3)); + StepVerifier.create(savedFlux).thenConsumeWhile(ReactiveTeacher -> true).expectComplete().verify(); + + List firstNames = new ArrayList<>(); + firstNames.add(TEACHER_FIRST_NAME_1); + final Flux resultsAsc = repository.annotatedFindByFirstNames(firstNames); + StepVerifier.create(resultsAsc) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_1)) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_2)) + .verifyComplete(); + + List firstNames2 = new ArrayList<>(); + firstNames2.add(TEACHER_FIRST_NAME_1); + firstNames2.add(TEACHER_FIRST_NAME_2); + final Flux resultsAsc2 = repository.annotatedFindByFirstNames(firstNames2); + StepVerifier.create(resultsAsc2) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_1)) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_2)) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_3)) + .verifyComplete(); + + } + + @Test + public void testAnnotatedQueryWithArrayContainsAndSort() { + final Mono deletedMono = repository.deleteAll(); + StepVerifier.create(deletedMono).thenAwait().verifyComplete(); + final Flux savedFlux = repository.saveAll(Arrays.asList(TEACHER_1, TEACHER_2, TEACHER_3)); + StepVerifier.create(savedFlux).thenConsumeWhile(ReactiveTeacher -> true).expectComplete().verify(); + + List firstNames = new ArrayList<>(); + firstNames.add(TEACHER_FIRST_NAME_1); + final Flux resultsAsc = repository.annotatedFindByFirstNamesWithSort(firstNames, Sort.by(Sort.Direction.DESC, "id")); + StepVerifier.create(resultsAsc) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_2)) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_1)) + .verifyComplete(); + + List firstNames2 = new ArrayList<>(); + firstNames2.add(TEACHER_FIRST_NAME_1); + firstNames2.add(TEACHER_FIRST_NAME_2); + final Flux resultsAsc2 = repository.annotatedFindByFirstNamesWithSort(firstNames2, Sort.by(Sort.Direction.DESC, "id")); + StepVerifier.create(resultsAsc2) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_3)) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_2)) + .expectNextMatches(teacher -> teacher.getId().equals(TEACHER_ID_1)) + .verifyComplete(); + + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/SpELCosmosAnnotationIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/SpELCosmosAnnotationIT.java new file mode 100644 index 000000000000..fc369a7ba29d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/SpELCosmosAnnotationIT.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.convert.ObjectMapperFactory; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.domain.SpELBeanStudent; +import com.azure.spring.data.cosmos.domain.SpELPropertyStudent; +import com.azure.spring.data.cosmos.repository.TestRepositorySpELConfig; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.databind.ObjectMapper; +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 = TestRepositorySpELConfig.class) +public class SpELCosmosAnnotationIT { + + private static final SpELPropertyStudent TEST_PROPERTY_STUDENT = new SpELPropertyStudent(TestConstants.ID_1, + TestConstants.FIRST_NAME, TestConstants.LAST_NAME); + + @Value("${cosmos.uri}") + private String dbUri; + + @Value("${cosmos.key}") + private String dbKey; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private CosmosTemplate cosmosTemplate; + + @Autowired + private CosmosConfig cosmosConfig; + + 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 CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder() + .endpoint(dbUri) + .key(dbKey); + CosmosAsyncClient client = CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + final CosmosFactory dbFactory = new CosmosFactory(client, TestConstants.DB_NAME); + + 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, cosmosConfig, mappingConverter); + + 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/SquareRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/SquareRepositoryIT.java new file mode 100644 index 000000000000..f4cf427b14b9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/SquareRepositoryIT.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.inheritance.Square; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.SquareRepository; +import org.junit.Before; +import org.junit.ClassRule; +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); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private SquareRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Square.class); + repository.save(square1); + repository.save(square2); + } + + @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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/StudentRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/StudentRepositoryIT.java new file mode 100644 index 000000000000..410f7ba9ec5e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/StudentRepositoryIT.java @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Student; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.StudentRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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 ID_4 = "id-4"; + public static final String ID_5 = "id-5"; + + 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 FIRST_NAME_4 = "Jack"; + + 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 LAST_NAME_4 = "lu"; + + 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 Student STUDENT_4 = new Student(ID_4, FIRST_NAME_4, LAST_NAME_4); + private static final Student STUDENT_5 = new Student(ID_5, FIRST_NAME_4, FIRST_NAME_4); + private static final List PEOPLE = + Arrays.asList(STUDENT_0, STUDENT_1, STUDENT_2, STUDENT_3, STUDENT_4, STUDENT_5); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private StudentRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Student.class); + this.repository.saveAll(PEOPLE); + } + + @Test + public void testFindByContaining() { + final List people = TestUtils.toList(repository.findByFirstNameContaining(SUB_FIRST_NAME)); + final List reference = Arrays.asList(STUDENT_1, STUDENT_2); + + assertPeopleEquals(people, reference); + } + + @Test + public void testFindByContainingWithAnd() { + final List people = TestUtils.toList(repository + .findByFirstNameContainingAndLastNameContaining("eng", "h")); + final List reference = Arrays.asList(STUDENT_1); + + assertPeopleEquals(people, reference); + } + + @Test + public void testFindByEndsWith() { + final List people = TestUtils.toList(repository.findByFirstNameEndsWith("en")); + final List reference = Arrays.asList(STUDENT_3); + + assertPeopleEquals(people, reference); + } + + @Test + public void testFindByNot() { + final List people = TestUtils.toList(repository.findByFirstNameNot("Mary")); + final List reference = Arrays.asList(STUDENT_1, STUDENT_2, STUDENT_3, STUDENT_4, STUDENT_5); + + assertPeopleEquals(people, reference); + } + + @Test + public void testFindByStartsWith() { + List people = TestUtils.toList(repository.findByFirstNameStartsWith("Z")); + + assertPeopleEquals(people, Arrays.asList(STUDENT_2, STUDENT_3)); + + people = TestUtils.toList(repository.findByLastNameStartsWith("C")); + + assertPeopleEquals(people, Arrays.asList(STUDENT_0, STUDENT_1)); + } + + @Test + public void testFindByStartsWithAndEndsWith() { + List people = TestUtils.toList(repository.findByFirstNameStartsWithAndLastNameEndingWith("Z", "H")); + + assertPeopleEquals(people, Arrays.asList(STUDENT_3)); + + people = TestUtils.toList(repository.findByFirstNameStartsWithAndLastNameEndingWith("Z", "en")); + + assertPeopleEquals(people, Arrays.asList()); + } + + @Test + public void testFindByStartsWithOrContaining() { + List people = TestUtils.toList(repository.findByFirstNameStartsWithOrLastNameContaining("Zhen", "C")); + + assertPeopleEquals(people, Arrays.asList(STUDENT_0, STUDENT_1, STUDENT_2, STUDENT_3)); + + people = TestUtils.toList(repository.findByFirstNameStartsWithOrLastNameContaining("M", "N")); + + assertPeopleEquals(people, Arrays.asList(STUDENT_0, STUDENT_2)); + } + + @Test + public void testFindByContainingAndNot() { + final List people = TestUtils.toList(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")); + } + + @Test + public void testFindByLastNameIgnoreCase() { + List people = TestUtils.toList(repository.findByLastNameIgnoreCase(LAST_NAME_0.toLowerCase())); + assertPeopleEquals(people, Arrays.asList(STUDENT_0)); + assertTrue(people.get(0).getLastName().equals(LAST_NAME_0)); + } + + @Test + public void testFindByLastNameAndFirstNameAllIgnoreCase() { + List people = TestUtils.toList(repository + .findByLastNameAndFirstNameAllIgnoreCase(LAST_NAME_0.toLowerCase(), FIRST_NAME_0.toLowerCase())); + assertPeopleEquals(people, Arrays.asList(STUDENT_0)); + assertTrue(people.get(0).getFirstName().equals(FIRST_NAME_0)); + assertTrue(people.get(0).getLastName().equals(LAST_NAME_0)); + } + + @Test + public void testFindByLastNameOrFirstNameAllIgnoreCase() { + List people = TestUtils.toList(repository + .findByLastNameOrFirstNameAllIgnoreCase(LAST_NAME_0.toLowerCase(), FIRST_NAME_1.toLowerCase())); + assertPeopleEquals(people, Arrays.asList(STUDENT_0, STUDENT_1)); + } + + @Test + public void testFindByFirstNameEndsWithIgnoreCase() { + List people = TestUtils.toList(repository + .findByFirstNameEndsWithIgnoreCase(FIRST_NAME_0.toLowerCase().substring(2))); + assertPeopleEquals(people, Arrays.asList(STUDENT_0)); + assertTrue(people.get(0).getFirstName().equals(FIRST_NAME_0)); + } + + @Test + public void testFindByLastNameStartsWithAndFirstNameStartsWithAllIgnoreCase() { + List people = TestUtils.toList(repository + .findByLastNameStartsWithAndFirstNameStartsWithAllIgnoreCase( + LAST_NAME_0.toLowerCase().substring(0, 2), FIRST_NAME_0.toLowerCase().substring(0, 2))); + assertPeopleEquals(people, Arrays.asList(STUDENT_0)); + assertTrue(people.get(0).getLastName().equals(LAST_NAME_0)); + assertTrue(people.get(0).getFirstName().equals(FIRST_NAME_0)); + } + + @Test + public void testFindByLastNameStartsWithOrFirstNameStartsWithAllIgnoreCase() { + List people = TestUtils.toList(repository + .findByLastNameStartsWithOrFirstNameStartsWithAllIgnoreCase( + LAST_NAME_0.toLowerCase().substring(0, 2), FIRST_NAME_1.toLowerCase().substring(0, 3))); + assertPeopleEquals(people, Arrays.asList(STUDENT_0, STUDENT_1)); + } + + @Test + public void testLimitingQuery() { + List people = TestUtils.toList(repository.findFirstByFirstName(FIRST_NAME_4)); + assertPeopleEquals(people, Arrays.asList(STUDENT_4)); + people = TestUtils.toList(repository.findFirst1ByFirstName(FIRST_NAME_4)); + assertPeopleEquals(people, Arrays.asList(STUDENT_4)); + people = TestUtils.toList(repository.findFirst2ByFirstName(FIRST_NAME_4)); + assertPeopleEquals(people, Arrays.asList(STUDENT_4, STUDENT_5)); + + people = TestUtils.toList(repository.findTopByFirstName(FIRST_NAME_4)); + assertPeopleEquals(people, Arrays.asList(STUDENT_4)); + people = TestUtils.toList(repository.findTop1ByFirstName(FIRST_NAME_4)); + assertPeopleEquals(people, Arrays.asList(STUDENT_4)); + people = TestUtils.toList(repository.findTop2ByFirstName(FIRST_NAME_4)); + assertPeopleEquals(people, Arrays.asList(STUDENT_4, STUDENT_5)); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/TeacherRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/TeacherRepositoryIT.java new file mode 100644 index 000000000000..c8be63dec53f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/TeacherRepositoryIT.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Teacher; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.TeacherRepository; +import org.junit.Before; +import org.junit.ClassRule; +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.Optional; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; + + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class TeacherRepositoryIT { + public static final String ID_0 = "id-0"; + + public static final String FIRST_NAME_0 = "Moary"; + + public static final String LAST_NAME_0 = "Chen"; + + private static final Teacher TEACHER_0 = new Teacher(ID_0, FIRST_NAME_0, LAST_NAME_0); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private TeacherRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, Teacher.class); + this.repository.save(TEACHER_0); + } + + @Test + public void testSaveWithSuppressedNullValue() { + this.repository.deleteAll(); + String teacherId = ID_0 + "-Other"; + final Teacher teacher = new Teacher(teacherId, FIRST_NAME_0, null); + Teacher save = repository.save(teacher); + assertNotNull(save); + + final Optional optionalTeacher = repository.findById(teacherId); + assertTrue(optionalTeacher.isPresent()); + assertTrue(optionalTeacher.get().getId().equals(teacherId)); + assertTrue(optionalTeacher.get().getFirstName().equals(FIRST_NAME_0)); + assertNull(optionalTeacher.get().getLastName()); + + assertTrue(repository.existsByFirstNameIsNotNull()); + assertFalse(repository.existsByLastNameIsNull()); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/UUIDIdDomainRepositoryIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/UUIDIdDomainRepositoryIT.java new file mode 100644 index 000000000000..83c2bd9cb1ae --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/UUIDIdDomainRepositoryIT.java @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.common.TestUtils; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.UUIDIdDomain; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.UUIDIdDomainRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +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.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class UUIDIdDomainRepositoryIT { + + private static final UUID ID_1 = UUID.randomUUID(); + private static final String NAME_1 = "moary"; + + private static final UUID ID_2 = UUID.randomUUID(); + private static final String NAME_2 = "camille"; + + private static final UUIDIdDomain DOMAIN_1 = new UUIDIdDomain(ID_1, NAME_1); + private static final UUIDIdDomain DOMAIN_2 = new UUIDIdDomain(ID_2, NAME_2); + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + @Autowired + private CosmosTemplate template; + + @Autowired + private UUIDIdDomainRepository repository; + + @Before + public void setUp() { + collectionManager.ensureContainersCreatedAndEmpty(template, UUIDIdDomain.class); + this.repository.save(DOMAIN_1); + this.repository.save(DOMAIN_2); + } + + @Test + public void testUUIDIdDomain() { + this.repository.deleteAll(); + Assert.assertFalse(this.repository.findById(ID_1).isPresent()); + + this.repository.save(DOMAIN_1); + final Optional foundOptional = this.repository.findById(ID_1); + + Assert.assertTrue(foundOptional.isPresent()); + Assert.assertEquals(DOMAIN_1.getNumber(), foundOptional.get().getNumber()); + Assert.assertEquals(DOMAIN_1.getName(), foundOptional.get().getName()); + + this.repository.delete(DOMAIN_1); + + Assert.assertFalse(this.repository.findById(ID_1).isPresent()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidDomain() { + new CosmosEntityInformation(InvalidDomain.class); + } + + @Test + public void testBasicQuery() { + final UUIDIdDomain save = this.repository.save(DOMAIN_1); + Assert.assertNotNull(save); + } + + @Test + public void testSaveAndFindById() { + Assert.assertNotNull(this.repository.save(DOMAIN_1)); + + final Optional savedEntity = this.repository.findById(DOMAIN_1.getNumber()); + Assert.assertTrue(savedEntity.isPresent()); + Assert.assertEquals(DOMAIN_1, savedEntity.get()); + } + + @Test + public void testSaveAllAndFindAll() { + Assert.assertTrue(this.repository.findAll().iterator().hasNext()); + + final Set entitiesToSave = Stream.of(DOMAIN_1, DOMAIN_2).collect(Collectors.toSet()); + this.repository.saveAll(entitiesToSave); + + final Set savedEntities = StreamSupport.stream(this.repository.findAll().spliterator(), false) + .collect(Collectors.toSet()); + + Assert.assertTrue(entitiesToSave.containsAll(savedEntities)); + } + + @Test + public void testFindAllById() { + final Iterable allById = + TestUtils.toList(this.repository.findAllById(Arrays.asList(DOMAIN_1.getNumber(), DOMAIN_2.getNumber()))); + Assert.assertTrue(((ArrayList) allById).size() == 2); + Iterator it = allById.iterator(); + assertUUIDIdDomainEquals(Arrays.asList(it.next(), it.next()), Arrays.asList(DOMAIN_1, DOMAIN_2)); + } + + private void assertUUIDIdDomainEquals(List cur, List reference) { + cur.sort(Comparator.comparing(UUIDIdDomain::getNumber)); + reference.sort(Comparator.comparing(UUIDIdDomain::getNumber)); + Assert.assertEquals(reference, cur); + } + + @Test + public void testCount() { + Assert.assertEquals(2, repository.count()); + } + + @Test + public void testDeleteById() { + this.repository.save(DOMAIN_1); + this.repository.save(DOMAIN_2); + this.repository.deleteById(DOMAIN_1.getNumber()); + this.repository.deleteById(DOMAIN_2.getNumber()); + Assert.assertEquals(0, this.repository.count()); + } + + @Test(expected = CosmosAccessException.class) + public void testDeleteByIdShouldFailIfNothingToDelete() { + this.repository.deleteAll(); + this.repository.deleteById(DOMAIN_1.getNumber()); + } + + @Test + public void testDelete() { + this.repository.save(DOMAIN_1); + this.repository.delete(DOMAIN_1); + Assert.assertEquals(1, this.repository.count()); + } + + @Test(expected = CosmosAccessException.class) + public void testDeleteShouldFailIfNothingToDelete() { + this.repository.deleteAll(); + this.repository.delete(DOMAIN_1); + } + + @Test + public void testDeleteAll() { + this.repository.save(DOMAIN_1); + this.repository.save(DOMAIN_2); + this.repository.deleteAll(Arrays.asList(DOMAIN_1, DOMAIN_2)); + Assert.assertEquals(0, this.repository.count()); + } + + @Test + public void testExistsById() { + this.repository.save(DOMAIN_1); + Assert.assertTrue(this.repository.existsById(DOMAIN_1.getNumber())); + } + + private static class InvalidDomain { + + private long count; + + private String location; + + InvalidDomain() { + } + + InvalidDomain(long count, String location) { + this.count = count; + this.location = location; + } + + public long getCount() { + return count; + } + + public void setCount(long 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/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/UniqueKeyPolicyIT.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/UniqueKeyPolicyIT.java new file mode 100644 index 000000000000..1aeaf5fb52ed --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/UniqueKeyPolicyIT.java @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.integration; + +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.UniqueKey; +import com.azure.cosmos.models.UniqueKeyPolicy; +import com.azure.spring.data.cosmos.IntegrationTestCollectionManager; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.domain.CompositeIndexEntity; +import com.azure.spring.data.cosmos.domain.UniqueKeyPolicyEntity; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; +import com.azure.spring.data.cosmos.repository.repository.UniqueKeyPolicyEntityRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.azure.spring.data.cosmos.repository.support.SimpleCosmosRepository; +import com.azure.spring.data.cosmos.repository.support.SimpleReactiveCosmosRepository; +import org.junit.Before; +import org.junit.ClassRule; +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.List; + +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 UniqueKeyPolicyIT { + + @ClassRule + public static final IntegrationTestCollectionManager collectionManager = new IntegrationTestCollectionManager(); + + private static final UniqueKeyPolicyEntity ENTITY_1 = new UniqueKeyPolicyEntity("id-1", "firstName-1", "lastName" + + "-1", "100", "city-1"); + private static final UniqueKeyPolicyEntity ENTITY_2 = new UniqueKeyPolicyEntity("id-2", "firstName-1", "lastName" + + "-2", "100", "city-2"); + private static final UniqueKeyPolicyEntity ENTITY_3 = new UniqueKeyPolicyEntity("id-3", "firstName-2", "lastName" + + "-3", "100", "city-1"); + private static final UniqueKeyPolicyEntity ENTITY_4 = new UniqueKeyPolicyEntity("id-4", "firstName-2", "lastName" + + "-4", "100", "city-2"); + private static final UniqueKeyPolicyEntity ENTITY_5 = new UniqueKeyPolicyEntity("id-5", "firstName-3", "lastName" + + "-5", "100", "city-3"); + + @Autowired + UniqueKeyPolicyEntityRepository repository; + + @Autowired + CosmosTemplate template; + + @Autowired + ReactiveCosmosTemplate reactiveTemplate; + + CosmosEntityInformation information = + new CosmosEntityInformation<>(UniqueKeyPolicyEntity.class); + + @Before + public void setup() { + collectionManager.ensureContainersCreatedAndEmpty(template, CompositeIndexEntity.class); + repository.saveAll(Arrays.asList(ENTITY_1, ENTITY_2, ENTITY_3, ENTITY_4, ENTITY_5)); + } + + @Test + public void canSetUniqueKeyPolicy() { + new SimpleCosmosRepository<>(information, template); + CosmosContainerProperties properties = template.getContainerProperties(information.getContainerName()); + UniqueKeyPolicy uniqueKeyPolicy = properties.getUniqueKeyPolicy(); + List uniqueKeys = uniqueKeyPolicy.getUniqueKeys(); + + assertThat(uniqueKeys.size()).isEqualTo(2); + + assertThat(uniqueKeys.get(0).getPaths().get(0)).isEqualTo("/lastName"); + assertThat(uniqueKeys.get(0).getPaths().get(1)).isEqualTo("/zipCode"); + + assertThat(uniqueKeys.get(1).getPaths().get(0)).isEqualTo("/city"); + } + + @Test + public void canSetUniqueKeyPolicyReactive() { + new SimpleReactiveCosmosRepository<>(information, reactiveTemplate); + CosmosContainerProperties properties = + reactiveTemplate.getContainerProperties(information.getContainerName()).block(); + UniqueKeyPolicy uniqueKeyPolicy = properties.getUniqueKeyPolicy(); + List uniqueKeys = uniqueKeyPolicy.getUniqueKeys(); + + assertThat(uniqueKeys.size()).isEqualTo(2); + + assertThat(uniqueKeys.get(0).getPaths().get(0)).isEqualTo("/lastName"); + assertThat(uniqueKeys.get(0).getPaths().get(1)).isEqualTo("/zipCode"); + + assertThat(uniqueKeys.get(1).getPaths().get(0)).isEqualTo("/city"); + } + + @Test + public void canSaveNewEntityWithDifferentUniqueKeys() { + long count = repository.count(); + assertThat(count).isEqualTo(5); + UniqueKeyPolicyEntity entity = new UniqueKeyPolicyEntity("id-6", "firstName-3", "lastName-6", + "100", "city-1"); + repository.save(entity); + count = repository.count(); + assertThat(count).isEqualTo(6); + repository.deleteById("id-6", new PartitionKey("firstName-3")); + count = repository.count(); + assertThat(count).isEqualTo(5); + } + + @Test + public void cannotSaveNewEntityWithUniqueKeysLastNameAndZipCode() { + // save with same lastName and zip code (which already exists in the same logical partition), though with a new id + UniqueKeyPolicyEntity entity = new UniqueKeyPolicyEntity("id-6", "firstName-3", "lastName-5", + "100", "city-6"); + try { + repository.save(entity); + fail("Save call should have failed with unique constraints exception"); + } catch (CosmosAccessException cosmosAccessException) { + assertThat(cosmosAccessException.getCosmosException().getStatusCode()).isEqualTo(409); + assertThat(cosmosAccessException.getCosmosException().getMessage()).contains("Unique index constraint " + + "violation."); + } + // change logical partition, now the entity should be saved + entity.setFirstName("firstName-2"); + repository.save(entity); + long count = repository.count(); + assertThat(count).isEqualTo(6); + repository.deleteById("id-6", new PartitionKey("firstName-2")); + count = repository.count(); + assertThat(count).isEqualTo(5); + } + + @Test + public void cannotSaveNewEntityWithUniqueKeysCity() { + // save with same city (which already exists in the same logical partition), though with a new id + UniqueKeyPolicyEntity entity = new UniqueKeyPolicyEntity("id-6", "firstName-3", "lastName-6", + "100", "city-3"); + try { + repository.save(entity); + fail("Save call should have failed with unique constraints exception"); + } catch (CosmosAccessException cosmosAccessException) { + assertThat(cosmosAccessException.getCosmosException().getStatusCode()).isEqualTo(409); + assertThat(cosmosAccessException.getCosmosException().getMessage()).contains("Unique index constraint " + + "violation."); + } + // change logical partition, now the entity should be saved + entity.setFirstName("firstName-2"); + repository.save(entity); + long count = repository.count(); + assertThat(count).isEqualTo(6); + repository.deleteById("id-6", new PartitionKey("firstName-2")); + count = repository.count(); + assertThat(count).isEqualTo(5); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AddressRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AddressRepository.java new file mode 100644 index 000000000000..9244d2ad4d5b --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AddressRepository.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import com.azure.spring.data.cosmos.repository.Query; +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.Param; +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); + + Iterable
findByPostalCodeAndCity(String postalCode, String city); + + Iterable
findByCity(String city); + + Iterable
findByCityIn(List cities); + + Iterable
findByPostalCode(String postalCode); + + Iterable
findByPostalCodeInAndCity(List postalCodes, String city); + + Iterable
findByStreetOrCity(String street, String city); + + @Query("select * from a where a.city = @city") + List
annotatedFindListByCity(@Param("city") String city); + + @Query("select * from a where a.city = @city") + Page
annotatedFindByCity(@Param("city") String city, Pageable pageable); + + @Query("select * from a where a.city = @city") + List
annotatedFindByCity(@Param("city") String city, Sort pageable); + + @Query("select DISTINCT value a.postalCode from a where a.city = @city") + Page annotatedFindPostalCodeValuesByCity(@Param("city") String city, Pageable pageable); + + @Query("select DISTINCT a.postalCode from a where a.city = @city") + Page annotatedFindPostalCodesByCity(@Param("city") String city, Pageable pageable); + + @Query("select DISTINCT a.postalCode from a where a.city = @city") + Slice annotatedFindPostalCodesByCityAsSlice(@Param("city") String city, Pageable pageable); + + @Query("select DISTINCT value a.postalCode from a where a.city = @city") + List annotatedFindPostalCodeValuesByCity(@Param("city") String city); + + @Query(value = "select * from a where a.city IN (@cities)") + List
annotatedFindByCityIn(@Param("cities") List cities, Sort sort); + + @Query(value = "SELECT * FROM a WHERE ARRAY_CONTAINS(@cities, a.city) ") + List
annotatedFindByCities(@Param("cities") List cities); + + @Query(value = "SELECT * FROM a WHERE ARRAY_CONTAINS(@cities, a.city) ") + List
annotatedFindByCitiesWithSort(@Param("cities") List cities, Sort sort); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AuditableIdGeneratedRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AuditableIdGeneratedRepository.java new file mode 100644 index 000000000000..b6fce91dfa31 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AuditableIdGeneratedRepository.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.AuditableIdGeneratedEntity; +import com.azure.spring.data.cosmos.repository.CosmosRepository; + +public interface AuditableIdGeneratedRepository extends CosmosRepository { +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AuditableRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AuditableRepository.java new file mode 100644 index 000000000000..5efbc81a8354 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/AuditableRepository.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.AuditableEntity; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import com.azure.spring.data.cosmos.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface AuditableRepository extends CosmosRepository { + + @Query("select * from r where r.id = @id") + List annotatedFindById(@Param("id") String id); + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ContactRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ContactRepository.java new file mode 100644 index 000000000000..fb8a8afaa7c8 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ContactRepository.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Contact; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import com.azure.spring.data.cosmos.repository.Query; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ContactRepository extends CosmosRepository { + Iterable findByTitle(String title); + + Iterable findByLogicId(String title); + + Contact findOneByTitle(String title); + + long countByTitle(String title); + + Long countByTitleAndIntValue(String title, int intValue); + + @Query(value = "select value count(1) from c where c.title = @title") + long countByQueryWithPrimitive(@Param("title") String title); + + @Query(value = "SELECT VALUE COUNT(1) from c where c.title = @title") + Long countByQueryWithNonPrimitive(@Param("title") String title); + + Optional findOptionallyByTitle(String title); + + @Query(value = "select * from c where c.title = @title and c.intValue = @value") + List getContactsByTitleAndValue(@Param("value") int value, @Param("title") String name); + + @Query(value = "select * from c offset @offset limit @limit") + List getContactsWithOffsetLimit(@Param("offset") int offset, @Param("limit") int limit); + + @Query(value = "select * from c where c.status= true") + List findActiveContacts(); + + @Query(value = "SELECT count(c.id) as id_count, c.intValue FROM c group by c.intValue") + List selectGroupBy(); + + @Query(value = "Select DISTINCT value c.intValue from c") + List findDistinctIntValueValues(); + + @Query(value = "Select DISTINCT value c.active from c") + List findDistinctStatusValues(); + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/CustomerRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/CustomerRepository.java new file mode 100644 index 000000000000..8f3195f1da08 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/CustomerRepository.java @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Customer; +import com.azure.spring.data.cosmos.repository.CosmosRepository; + +public interface CustomerRepository extends CosmosRepository { + Iterable findByUser_Name(String name); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/IntegerIdDomainRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/IntegerIdDomainRepository.java new file mode 100644 index 000000000000..eb064a5f9478 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/IntegerIdDomainRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.IntegerIdDomain; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface IntegerIdDomainRepository extends CosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/LongIdDomainRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/LongIdDomainRepository.java new file mode 100644 index 000000000000..c8fb082d6691 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/LongIdDomainRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.LongIdDomain; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface LongIdDomainRepository extends CosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/MemoRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/MemoRepository.java new file mode 100644 index 000000000000..a4a484257754 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/MemoRepository.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Importance; +import com.azure.spring.data.cosmos.domain.Memo; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +import java.util.Date; + +@Repository +public interface MemoRepository extends CosmosRepository { + Iterable findMemoByDate(Date date); + + Iterable findMemoByImportance(Importance importance); + + Iterable findByDateBefore(Date date); + + Iterable findByDateBeforeAndMessage(Date date, String message); + + Iterable findByDateBeforeOrMessage(Date date, String message); + + Iterable findByDateAfter(Date date); + + Iterable findByDateAfterAndMessage(Date date, String message); + + Iterable findByDateAfterOrMessage(Date date, String message); + + Iterable findByDateBetween(Date startDate, Date endDate); + + Iterable findByDateBetweenAndMessage(Date startDate, Date endDate, String message); + + Iterable findByDateBetweenOrMessage(Date startDate, Date endDate, String message); + + Iterable findByMessageStartsWith(String message); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/NestedPartitionKeyRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/NestedPartitionKeyRepository.java new file mode 100644 index 000000000000..f63e3d423662 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/NestedPartitionKeyRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.NestedPartitionKeyEntityWithGeneratedValue; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface NestedPartitionKeyRepository extends CosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageableAddressRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageableAddressRepository.java new file mode 100644 index 000000000000..32ebe73917ca --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageableAddressRepository.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +public interface PageableAddressRepository extends CosmosRepository { + Page
findByStreet(String street, Pageable pageable); + + Page
findByCity(String city, Pageable pageable); + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageableMemoRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageableMemoRepository.java new file mode 100644 index 000000000000..ed3a37bf7934 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageableMemoRepository.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Importance; +import com.azure.spring.data.cosmos.domain.PageableMemo; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Repository; + +@Repository +public interface PageableMemoRepository extends CosmosRepository { + Slice findByImportance(Importance importance, Pageable pageable); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageablePersonRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageablePersonRepository.java new file mode 100644 index 000000000000..ecc5dbb5beb6 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PageablePersonRepository.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.PageablePerson; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PageablePersonRepository extends CosmosRepository { +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersistableEntityRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersistableEntityRepository.java new file mode 100644 index 000000000000..39208672580b --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersistableEntityRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.PersistableEntity; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PersistableEntityRepository extends CosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersonWithEtagRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersonWithEtagRepository.java new file mode 100644 index 000000000000..940cad0f1f52 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersonWithEtagRepository.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.PersonWithEtag; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PersonWithEtagRepository extends CosmosRepository { +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ProjectRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ProjectRepository.java new file mode 100644 index 000000000000..b7f26d178aec --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ProjectRepository.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Project; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import java.util.Collection; + +public interface ProjectRepository extends CosmosRepository { + + Iterable findByNameAndStarCount(String name, Long startCount); + + Iterable findByNameOrForkCount(String name, Long forkCount); + + Iterable findByNameAndCreator(String name, String creator); + + Iterable findByNameAndCreatorOrNameAndCreator(String name, String creator, String name2, String creator2); + + Iterable findByNameOrCreator(String name, String creator); + + Iterable findByNameAndCreatorOrForkCount(String name, String creator, Long forkCount); + + Iterable findByNameOrCreatorAndForkCount(String name, String creator, Long forkCount); + + Iterable findByNameOrCreatorOrForkCount(String name, String creator, Long forkCount); + + Iterable findByNameOrCreatorAndForkCountOrStarCount(String name, String creator, + Long forkCount, Long starCount); + + Iterable findByForkCountGreaterThan(Long forkCount); + + Iterable findByCreatorAndForkCountGreaterThan(String creator, Long forkCount); + + Iterable findByCreatorOrForkCountGreaterThan(String creator, Long forkCount); + + Iterable findByNameOrCreator(String name, String creator, Sort sort); + + Iterable findByNameAndCreator(String name, String creator, Sort sort); + + Iterable findByForkCount(Long forkCount, Sort sort); + + Iterable findByStarCountLessThan(Long starCount); + + Iterable findByForkCountLessThanEqual(Long forkCount); + + Iterable findByStarCountLessThanAndForkCountGreaterThan(Long max, Long min); + + Iterable findByForkCountLessThanEqualAndStarCountGreaterThan(Long max, Long min); + + Iterable findByStarCountGreaterThanEqual(Long count); + + Iterable findByForkCountGreaterThanEqualAndCreator(Long count, String creator); + + Iterable findByHasReleasedTrue(); + + Iterable findByHasReleasedFalse(); + + Iterable findByHasReleasedTrueAndCreator(String creator); + + Iterable findByHasReleasedFalseAndCreator(String creator); + + Iterable findByHasReleasedTrueOrCreator(String creator); + + Iterable findByHasReleasedFalseOrCreator(String creator); + + Iterable findByCreatorIn(Collection creators); + + Iterable findByCreatorInAndStarCountIn(Collection creators, Collection starCounts); + + Iterable findByCreatorInOrStarCount(Collection creators, Long starCount); + + Iterable findByCreatorNotIn(Collection creators); + + Iterable findByCreatorInAndStarCountNotIn(Collection creators, Collection starCounts); + + Iterable findByNameIsNull(); + + Iterable findByNameIsNullAndForkCount(Long forkCount); + + Iterable findByNameIsNotNull(); + + Iterable findByNameIsNotNullAndHasReleased(boolean hasReleased); + + Page findByForkCount(Long forkCount, Pageable pageable); + + Iterable findAllByNameIn(Collection names); + + Iterable findAllByStarCountIn(Collection startCounts); + + Iterable findAllByHasReleasedIn(Collection releases); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/QuestionRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/QuestionRepository.java new file mode 100644 index 000000000000..f45d4621ec9e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/QuestionRepository.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Question; +import com.azure.spring.data.cosmos.repository.CosmosRepository; + +public interface QuestionRepository extends CosmosRepository { +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAddressRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAddressRepository.java new file mode 100644 index 000000000000..67bdc0af405c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAddressRepository.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.repository.Query; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Repository +public interface ReactiveAddressRepository extends ReactiveCosmosRepository { + Mono deleteByCity(String city); + + Flux
findByCityIn(List cities); + + @Query("select * from a where a.city = @city") + Flux
annotatedFindListByCity(@Param("city") String city); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAuditableIdGeneratedRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAuditableIdGeneratedRepository.java new file mode 100644 index 000000000000..30669e5b0592 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAuditableIdGeneratedRepository.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.AuditableIdGeneratedEntity; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; + +public interface ReactiveAuditableIdGeneratedRepository extends ReactiveCosmosRepository { +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAuditableRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAuditableRepository.java new file mode 100644 index 000000000000..819846e8c89f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveAuditableRepository.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.AuditableEntity; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; + +public interface ReactiveAuditableRepository extends ReactiveCosmosRepository { +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseRepository.java new file mode 100644 index 000000000000..e32a7c7d4955 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseRepository.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Course; +import com.azure.spring.data.cosmos.repository.Query; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.data.repository.query.Param; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; + +public interface ReactiveCourseRepository extends ReactiveCosmosRepository { + + Flux findByDepartmentIn(Collection departments); + + /** + * Find Course list by name without case sensitive + * @param name name + * @return Course list + */ + Flux findByNameIgnoreCase(String name); + + /** + * Find Course list by name and department without case sensitive + * @param name name + * @param department department + * @return Course list + */ + Flux findByNameAndDepartmentAllIgnoreCase(String name, String department); + + /** + * Find Course list by (name and department) or (name2 and department2) + * @param name name + * @param department department + * @param name2 name2 + * @param department2 department2 + * @return Course list + */ + Flux findByNameAndDepartmentOrNameAndDepartment(String name, + String department, String name2, String department2); + + /** + * Find Course list by name or department without case sensitive + * @param name name + * @param department department + * @return Course list + */ + Flux findByNameOrDepartmentAllIgnoreCase(String name, String department); + + @Query(value = "select c as jsonNode from c where c.name = @name") + Flux annotatedFindByName(@Param("name") String city); + + /** + * Find a single Course list by name + * @param name name + * @return Course list + */ + Mono findOneByName(String name); + + @Query(value = "select * from c where c.name = @name and c.department = @department") + Flux getCoursesWithNameDepartment(@Param("name") String name, @Param("department") String department); + + @Query(value = "select count(c.id) as num_ids, c.department from c group by c.department") + Flux getCoursesGroupByDepartment(); + + Mono countByName(String name); + + @Query(value = "select value count(1) from c where c.name = @name") + Mono countByQuery(@Param("name") String name); + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseWithEtagRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseWithEtagRepository.java new file mode 100644 index 000000000000..e5a009fbca50 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseWithEtagRepository.java @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.CourseWithEtag; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; + +public interface ReactiveCourseWithEtagRepository extends ReactiveCosmosRepository { +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveLongIdDomainPartitionRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveLongIdDomainPartitionRepository.java new file mode 100644 index 000000000000..a6adc573650f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveLongIdDomainPartitionRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.LongIdDomainPartition; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ReactiveLongIdDomainPartitionRepository extends ReactiveCosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveNestedPartitionKeyRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveNestedPartitionKeyRepository.java new file mode 100644 index 000000000000..6a8ccebc802d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveNestedPartitionKeyRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.NestedPartitionKeyEntity; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ReactiveNestedPartitionKeyRepository extends ReactiveCosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactivePersistableEntityRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactivePersistableEntityRepository.java new file mode 100644 index 000000000000..c562bfba8bb5 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactivePersistableEntityRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.PersistableEntity; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ReactivePersistableEntityRepository extends ReactiveCosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveRoleRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveRoleRepository.java new file mode 100644 index 000000000000..cbf264c2dd3c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveRoleRepository.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Role; +import com.azure.spring.data.cosmos.repository.Query; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.Param; +import reactor.core.publisher.Flux; + +import java.util.List; + +public interface ReactiveRoleRepository extends ReactiveCosmosRepository { + + Flux findByDeveloperAndId(boolean isDeveloper, String id); + + @Query(value = "select * from c where c.id = @id") + Flux annotatedFindRoleById(@Param("id") String id); + + @Query(value = "select * from c where c.developer = true and c.level = @level") + Flux annotatedFindDeveloperByLevel(@Param("level") String level, Sort sort); + + @Query(value = "select * from c where c.level IN (@levels)") + Flux annotatedFindRoleByLevelIn(@Param("levels") List levels, Sort sort); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveTeacherRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveTeacherRepository.java new file mode 100644 index 000000000000..797e98d3337e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveTeacherRepository.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.ReactiveTeacher; +import com.azure.spring.data.cosmos.repository.Query; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.Param; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + + +public interface ReactiveTeacherRepository extends ReactiveCosmosRepository { + + Mono existsByFirstNameIsNotNull(); + + Mono existsByLastNameIsNull(); + + @Query(value = "SELECT * FROM a WHERE ARRAY_CONTAINS(@firstNames, a.firstName) ") + Flux annotatedFindByFirstNames(@Param("firstNames") List firstNames); + + @Query(value = "SELECT * FROM a WHERE ARRAY_CONTAINS(@firstNames, a.firstName) ") + Flux annotatedFindByFirstNamesWithSort(@Param("firstNames") List firstNames, Sort sort); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveUUIDIdDomainRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveUUIDIdDomainRepository.java new file mode 100644 index 000000000000..d29399062b42 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveUUIDIdDomainRepository.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.UUIDIdDomain; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface ReactiveUUIDIdDomainRepository extends ReactiveCosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/SortedProjectRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/SortedProjectRepository.java new file mode 100644 index 000000000000..23394f37e8a1 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/SortedProjectRepository.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.SortedProject; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +public interface SortedProjectRepository extends CosmosRepository { + + Iterable findByNameOrCreator(String name, String creator, Sort sort); + + Iterable findByNameAndCreator(String name, String creator, Sort sort); + + Iterable findByForkCount(Long forkCount, Sort sort); + + Page findByForkCount(Long forkCount, Pageable pageable); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/SquareRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/SquareRepository.java new file mode 100644 index 000000000000..a46748b0c523 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/SquareRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.inheritance.Square; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SquareRepository extends CosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/StudentRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/StudentRepository.java new file mode 100644 index 000000000000..dcd86cdc3036 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/StudentRepository.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Student; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface StudentRepository extends CosmosRepository { + + Iterable findByFirstNameContaining(String firstName); + + Iterable findByFirstNameContainingAndLastNameContaining(String firstName, String lastName); + + Iterable findByFirstNameEndsWith(String firstName); + + Iterable findByFirstNameStartsWith(String firstName); + + Iterable findByLastNameStartsWith(String lastName); + + Iterable findByFirstNameStartsWithAndLastNameEndingWith(String firstName, String lastName); + + Iterable findByFirstNameStartsWithOrLastNameContaining(String firstName, String lastName); + + Iterable findByFirstNameNot(String firstName); + + Iterable findByFirstNameContainingAndLastNameNot(String firstName, String lastName); + + Boolean existsByFirstName(String firstName); + + Boolean existsByLastNameContaining(String lastName); + + /** + * Find student list by last name without case sensitive + * + * @param lastName last name + * @return student Iterable + */ + Iterable findByLastNameIgnoreCase(String lastName); + + /** + * Find student list by last name and first name without case sensitive + * + * @param lastName last name + * @param firstName first name + * @return student Iterable + */ + Iterable findByLastNameAndFirstNameAllIgnoreCase(String lastName, String firstName); + + /** + * Find student list by last name or first name without case sensitive + * + * @param lastName last name + * @param firstName first name + * @return Student Iterable + */ + Iterable findByLastNameOrFirstNameAllIgnoreCase(String lastName, String firstName); + + /** + * Find student list by first name matching end without case sensitive + * + * @param firstName first name + * @return Student Iterable + */ + Iterable findByFirstNameEndsWithIgnoreCase(String firstName); + + /** + * Find student list by last name matching end and first name matching end without case sensitive + * + * @param lastName last name + * @param firstName first name + * @return Student Iterable + */ + Iterable findByLastNameStartsWithAndFirstNameStartsWithAllIgnoreCase(String lastName, String firstName); + + /** + * Find student list by last name matching or and first name matching end without case sensitive + * + * @param lastName last name + * @param firstName first name + * @return Student Iterable + */ + Iterable findByLastNameStartsWithOrFirstNameStartsWithAllIgnoreCase(String lastName, String firstName); + + Iterable findFirstByFirstName(String firstName); + + Iterable findFirst1ByFirstName(String firstName); + + Iterable findFirst2ByFirstName(String firstName); + + Iterable findTopByFirstName(String firstName); + + Iterable findTop1ByFirstName(String firstName); + + Iterable findTop2ByFirstName(String firstName); + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/TeacherRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/TeacherRepository.java new file mode 100644 index 000000000000..3c7a566d2ad9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/TeacherRepository.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Teacher; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + + +@Repository +public interface TeacherRepository extends CosmosRepository { + + boolean existsByFirstNameIsNotNull(); + + boolean existsByLastNameIsNull(); +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/UUIDIdDomainRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/UUIDIdDomainRepository.java new file mode 100644 index 000000000000..c15c2d26db2f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/UUIDIdDomainRepository.java @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.UUIDIdDomain; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface UUIDIdDomainRepository extends CosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/UniqueKeyPolicyEntityRepository.java b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/UniqueKeyPolicyEntityRepository.java new file mode 100644 index 000000000000..59dd46f30164 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/UniqueKeyPolicyEntityRepository.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.UniqueKeyPolicyEntity; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UniqueKeyPolicyEntityRepository extends CosmosRepository { + +} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/resources/application.properties b/sdk/spring/azure-spring-data-cosmos-test/src/test/resources/application.properties new file mode 100644 index 000000000000..18af39dea3c1 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/resources/application.properties @@ -0,0 +1,27 @@ +cosmos.uri=${COSMOS_SPRING_ACCOUNT_HOST} +cosmos.key=${COSMOS_SPRING_ACCOUNT_KEY} +cosmos.secondaryKey=${COSMOS_SPRING_SECONDARY_ACCOUNT_KEY} + +dynamic.collection.name=spel-property-collection +# Populate query metrics +cosmos.queryMetricsEnabled=true +# Max Degree of Parallelism allowed +cosmos.maxDegreeOfParallelism=0 +# Max number of items to buffer +cosmos.maxBufferedItemCount=0 +# Max size of the response continuation token +cosmos.responseContinuationTokenLimitInKb=0 + +# Secondary DataSource Config +cosmos.secondary.uri=${COSMOS_SPRING_NEW_ACCOUNT_HOST} +cosmos.secondary.key=${COSMOS_SPRING_NEW_ACCOUNT_KEY} +cosmos.secondary.secondaryKey=${COSMOS_SPRING_NEW_SECONDARY_ACCOUNT_KEY} + +# Populate query metrics +cosmos.secondary.queryMetricsEnabled=true +# Max Degree of Parallelism allowed +cosmos.secondary.maxDegreeOfParallelism=0 +# Max number of items to buffer +cosmos.secondary.maxBufferedItemCount=0 +# Max size of the response continuation token +cosmos.secondary.responseContinuationTokenLimitInKb=0 diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/resources/application.yml b/sdk/spring/azure-spring-data-cosmos-test/src/test/resources/application.yml new file mode 100644 index 000000000000..800b67277900 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/src/test/resources/application.yml @@ -0,0 +1,3 @@ +cosmosdb: + key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== + uri: ${DOCUMENTDB_URI} diff --git a/sdk/spring/azure-spring-data-cosmos-test/src/test/resources/logback-test.xml b/sdk/spring/azure-spring-data-cosmos-test/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..e0a557553523 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos-test/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/spring/azure-spring-data-cosmos/CHANGELOG.md b/sdk/spring/azure-spring-data-cosmos/CHANGELOG.md new file mode 100644 index 000000000000..532baf4599d4 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/CHANGELOG.md @@ -0,0 +1,348 @@ +## Release History + +### 3.35.0-beta.1 (Unreleased) + +#### Features Added + +#### Breaking Changes + +#### Bugs Fixed + +#### Other Changes +* The module `azure-spring-data-cosmos` was moved from sdk/cosmos to sdk/spring - See [PR 33905](https://github.com/Azure/azure-sdk-for-java/pull/33905) + +### 3.34.0 (2023-04-21) +#### Breaking Changes +* Added a new flag `overwritePolicy` to `CosmosIndexingPolicy` that when set to true (by default it is false) will allow the user to overwrite the Indexing policy in Portal using the Indexing Policy defined in the SDK. This will affect users who change the Indexing Policy they have defined on the container and want that to overwrite what is in portal, you will now need to set the flag `overwritePolicy` to true for this to happen. The reason we have added this breaking change is that allowing overwrite of an existing indexing policy is considered too risky to be a default behavior. The risk is that you may be removing indexes through multiple indexing policy changes, and in that case the query engine may not provide consistent or complete results until all index transformations are complete. So we are changing the default behavior so that users must opt in to overwriting the indexing policy that exists. - See [PR 33171](https://github.com/Azure/azure-sdk-for-java/pull/33171) + +#### Bugs Fixed +* Fixing ARRAY_CONTAINS annotated query bug in Reactive Spring introduced by fixing to IN annotated queries. - See [PR 34274](https://github.com/Azure/azure-sdk-for-java/pull/34274) + +#### Other Changes +* Updated `azure-cosmos` to version `4.44.0`. + +### 3.33.0 (2023-03-17) +#### Bugs Fixed +* Bug fixed in `ReactiveCosmosTemplate` where returning a Flux was causing an error - See [PR 33730](https://github.com/Azure/azure-sdk-for-java/pull/33730) + +#### Other Changes +* Updated `azure-cosmos` to version `4.42.0`. +* Updated `Spring Data Commons` to version `2.7.8`. + +### 3.32.0 (2023-02-17) +#### Features Added +* Added support for multi-tenancy at the Container level via `CosmosFactory` - See [PR 33400](https://github.com/Azure/azure-sdk-for-java/pull/33400) + +#### Other Changes +* Updated `azure-cosmos` to version `4.41.0`. +* Updated `Spring Boot` to version `2.7.8`. +* Updated `Spring Data Commons` to version `2.7.7`. + +### 3.31.0 (2023-01-13) +#### Features Added +* Added support for multi-tenancy at the Database level via `CosmosFactory` - See [PR 32516](https://github.com/Azure/azure-sdk-for-java/pull/32516) +* Added support for Patch API in `CosmosRepository` and `ReactiveCosmosRepository` - See [PR 32630](https://github.com/Azure/azure-sdk-for-java/pull/32630) + +### Other Changes +* Updated `azure-cosmos` to version `4.40.0`. +* Updated `Spring Boot` to version `2.7.7`. +* Updated `Spring Data Commons` to version `2.7.6`. + +### 3.30.0 (2022-11-16) +### Other Changes +* Updated `azure-cosmos` to version `4.39.0`. + +### 3.29.1 (2022-10-21) +#### Bugs Fixed +* Fixed serialization and persistence issues for UUID for JDK 16 and above - See [PR 31417](https://github.com/Azure/azure-sdk-for-java/pull/31417) + +#### Other Changes +* Updated `azure-cosmos` to version `4.38.1`. +* `azure-cosmos` version 4.38.1 fixes two CVEs related to jackson-databind and apache commons-text dependencies. + +### 3.29.0 (2022-10-12) +#### Other Changes +* Updated `azure-cosmos` to version `4.38.0`. + +### 3.28.1 (2022-10-07) +> [!IMPORTANT] +> We strongly recommend our customers to use version 3.28.1 and above. +#### Other Changes +* Updated `azure-cosmos` to version `4.37.1`. + +### 3.28.0 (2022-09-30) +#### Bugs Fixed +* Fixing ARRAY_CONTAINS annotated query bug introduced by fixing to IN annotated queries. - See [PR 31179](https://github.com/Azure/azure-sdk-for-java/pull/31179) + +#### Other Changes +* Updated `azure-cosmos` to version `4.37.0`. + +### 3.27.0 (2022-09-15) +#### Features Added +* Exposed `maxBufferedItemCount` feature from `CosmosQueryRequestOptions` through `application.properties` flag - See [PR 30921](https://github.com/Azure/azure-sdk-for-java/pull/30921) +* Exposed `responseContinuationTokenLimitInKb` feature from `CosmosQueryRequestOptions` through `application.properties` flag - See [PR 30980](https://github.com/Azure/azure-sdk-for-java/pull/30980) + +#### Bugs Fixed +* Fixing pagination bug when performing a cross-partition query to fill every page and fix the total page count reporting. - See [PR 30694](https://github.com/Azure/azure-sdk-for-java/pull/30694) + +#### Other Changes +* Updated `azure-cosmos` to version `4.36.0`. + +### 3.26.0 (2022-08-19) +#### Features Added +* Added support for NOT CONTAINS. - See [PR 30379](https://github.com/Azure/azure-sdk-for-java/pull/30379) + +#### Bugs Fixed +* Fixed issues with pagination when an offset is passed in with the pageable object. - See [PR 29462](https://github.com/Azure/azure-sdk-for-java/pull/29462) +* Fixed an issue with @Query annotation using IN queries in `azure-spring-data-cosmos` which were not working - See [PR 30123](https://github.com/Azure/azure-sdk-for-java/pull/30123) +* Fixed sorted queries to utilize composite indexes. - See [PR 30199](https://github.com/Azure/azure-sdk-for-java/pull/30199) +* Fixed issues with pagination when accessing a page other than the first page. - See [PR 30276](https://github.com/Azure/azure-sdk-for-java/pull/30276) + +#### Other Changes +* Updated `azure-cosmos` to version `4.35.0`. + +### 3.25.0 (2022-07-22) +#### Bugs Fixed +* Fixed issues with offset and limit where you cannot use offset and limit. - See [PR 29841](https://github.com/Azure/azure-sdk-for-java/pull/29841) + +#### Other Changes +* Updated `azure-cosmos` to version `4.33.1`. + +### 3.24.0 (2022-07-14) +#### Other Changes +* Updated `azure-cosmos` to version `4.33.0`. + +### 3.23.0 (2022-06-27) +> [!IMPORTANT] +> This release supports Spring Boot version 2.7.x and above. +#### Features Added +* Updated `azure-cosmos` to version `4.32.0`. +* Updated `Spring Boot` to version `2.7.1`. +* Updated `Spring Data Commons` to version `2.7.1`. +#### Other Changes +* Performance improvement for case-insensitive queries - See [PR 29597](https://github.com/Azure/azure-sdk-for-java/pull/29597) and [PR 29644](https://github.com/Azure/azure-sdk-for-java/pull/29644) + +### 3.22.0 (2022-06-08) +#### Features Added +* Exposed `maxDegreeOfParallelism` feature from CosmosQueryRequestOptions through application.properties flag - See [PR 28756](https://github.com/Azure/azure-sdk-for-java/pull/28756) + +#### Other Changes +* Updated `azure-cosmos` to version `4.31.0`. + +### 3.21.0 (2022-05-20) +#### Other Changes +* Updated `azure-cosmos` to version `4.30.0`. +* Updated `Spring Boot` to version `2.6.7`. + +### 3.20.0 (2022-04-22) +#### Other Changes +* Updated `azure-cosmos` to version `4.29.0`. +* Updated `Spring Boot` to version `2.6.6`. + +### 3.19.1 (2022-04-08) +#### Other Changes +* Updated `azure-cosmos` to version `4.28.1`. + +### 3.19.0 (2022-03-10) +#### Features Added +* Updated `Spring Boot` to version `2.6.3`. +* Updated `azure-cosmos` to version `4.27.0`. +* Added support for container `UniqueKey` policies - See [PR 27270](https://github.com/Azure/azure-sdk-for-java/pull/27270) + +#### Bugs Fixed +* Fixed an issue with `userAgent` in `azure-spring-data-cosmos` being overridden by other spring modules - See [PR 27311](https://github.com/Azure/azure-sdk-for-java/pull/27311) + +### 3.18.0 (2022-02-11) +#### Features Added +* Updated `azure-cosmos` to version `4.26.0`. + +### 3.17.0 (2022-01-14) +#### Features Added +* Updated `Spring Boot` to version `2.6.2`. +* Updated `azure-cosmos` to version `4.25.0`. + +### 3.16.0 (2021-12-21) +#### Features Added +* Updated `azure-cosmos` to version `4.24.0`. + +### 3.15.0 (2021-12-10) +#### Features Added +* Updated `Spring Boot` to version `2.6.1`. +* Updated `azure-cosmos` to version `4.23.0`. + +#### Bugs Fixed +* Added support for capturing CosmosException diagnostics on `CosmosRepository` and `ReactiveRepository` APIs. +* Solved `UnsatisfiedDependencyException` when upgrading to spring boot 2.6.1 by removing `CosmosMappingContextLookup`. + +### 3.14.0 (2021-11-12) +#### Features Added +* Updated `azure-cosmos` to version `4.21.0`. + +#### Bugs Fixed +* Fixed issue with spring onLoad event not firing for paged queries. +* Exceptions thrown from spring application event listeners for onLoad events will now propagate up the call stack. + +### 3.13.1 (2021-10-27) +#### Bugs Fixed +* Fixed support for slice query in annotated `@Query` queries. +* Fixed issue with preserving priority for closed queries. +* Updated `azure-cosmos` to version `4.20.1`. + +### 3.13.0 (2021-10-14) +#### New Features +* Added support for setting throughput on database creation. +* Pagination improvement with slice query API. +* Updated `azure-cosmos` to version `4.20.0`. + +### 3.12.0 (2021-09-24) +#### New Features +* Added support for UUID id type to spring data cosmos SDK. +* Updated `azure-cosmos` to version 4.19.1. + +### 3.11.0 (2021-09-09) +#### New Features +* Added Spring `ApplicationEventListener` support. +* Updated `Spring Boot` to version 2.5.4. +* Updated `azure-cosmos` to version 4.19.0. + +#### Bugs Fixed +* Fixed spring data cosmos `query plan` caching. +* Fixed query parameter name generation for nested properties containing space. + +### 3.11.0-beta.1 (2021-09-02) +#### New Features +* Updated `azure-cosmos` to version 4.19.0-beta.1. + +#### Bugs Fixed +* Fixed spring data cosmos `query plan` caching. + +### 3.10.0 (2021-08-16) +#### New Features +* Updated `Spring Boot` to version 2.5.3. +* Updated `azure-cosmos` to version 4.18.0. + +### 3.9.0 (2021-07-08) +#### New Features +* Added `PartitionKey` support to spring data single partition queries. +* Updated `Spring Boot` to version 2.5.2. +* Updated `azure-cosmos` to version 4.17.0. + +### 3.8.0 (2021-06-11) +#### New Features +* Updated `Spring Boot` to version 2.5.0. +* Updated `azure-cosmos` to version 4.16.0. +* Added Autoscale RU support feature. +* Added support for `countBy*` methods on `Repository` and `@Query` annotation. + +#### Bugs Fixed +* Fixed an issue with `MappingCosmosConverter` handling query with value types. +* Fixed an issue with `CosmosIndexingPolicy` getting reset on application bootup. + +### 3.7.0 (2021-05-12) +#### New Features +* Updated Spring Boot to [spring-boot-dependencies:2.4.5](https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/2.4.5/). +* Updated Spring Cloud to [spring-cloud-dependencies:2020.0.2](https://repo.maven.apache.org/maven2/org/springframework/cloud/spring-cloud-dependencies/2020.0.2/). +* Added `slice` support for queries that do not require page count. +* Updated `azure-cosmos` to version 4.15.0. + +### 3.6.0 (2021-04-06) +#### New Features +* Updated `azure-cosmos` to version 4.14.0. + +### 3.5.1 (2021-03-24) +#### Bugs Fixed +* Updated `azure-cosmos` to hotfix version 4.13.1. + +### 3.5.0 (2021-03-11) +> [!IMPORTANT] +> This release supports spring boot 2.4.3 and above. +#### New Features +* Updated `spring-boot` to major version 2.4.3. +* Updated `spring-core` to major version 5.3.4. +* Updated `spring-data-commons` to major version 2.4.5. +* Updated `azure-cosmos` version to 4.13.0. +* Added support for `org.springframework.data.domain.Persistable` entity type. +* Added support to log SQL Queries. +* Added support for `Pageable` and `Sort` for `@query` annotated queries. + +#### Bugs Fixed +* Fixed issue when using automatic id generation with the auditable framework. +* Fixed query deserialization issue with `@query` annotated queries. + +### 3.4.0 (2021-02-09) +#### New Features +* Updated `azure-cosmos` version to 4.12.0. + +#### Bugs Fixed +* Fixed `@EnableAuditing` for Java 11 modules. + +### 3.3.0 (2021-01-15) +#### New Features +* Support for composite indexes in `CosmosIndexPolicy` annotation. +* Support for changes in `CosmosIndexPolicy` annotation. +* Updated azure-cosmos version to 4.11.0. + +#### Bugs Fixed +* Fixed query generation with sort and limit. + +### 3.2.0 (2020-12-11) +#### New Features +* Updated Spring Data Commons version to 2.3.5.RELEASE. +* Updated Spring Core version to 5.2.10.RELEASE. + +#### Bugs Fixed +* Fixed publishing of `spring.factories` file with released jar. +* Fixed repository query with repeated parameters. + +### 3.1.0 (2020-10-21) +#### New Features +* Added support for `ARRAY_CONTAINS` `CriteriaType`. +* Updated azure-cosmos version to 4.7.1. + +#### Bugs Fixed +* Fixed an issue where annotated queries do not pick the annotated container name. + +### 3.0.0 (2020-09-30) +#### New Features +* Updated azure-cosmos dependency to `4.6.0` + +### 3.0.0-beta.2 (2020-09-17) +#### New Features +* Updated artifact id to `azure-spring-data-cosmos`. +* Updated azure-cosmos dependency to `4.5.0`. +* `Query Annotation` support for native queries. +* Support for Java 11. +* Added support for Nested Partition Key by exposing `partitionKeyPath` field in `@Container` annotation. +* Added support for `limit` query type allowing `top` and `first` to be used when defining repository APIs. +#### Bugs Fixed +* Fixed nested partition key bug when used with `@GeneratedValue` annotation. + +### 3.0.0-beta.1 (2020-08-17) +#### New Features +* Updated group id to `com.azure`. +* Updated artifact id to `azure-spring-data-cosmos-core`. +* Updated azure-cosmos SDK dependency to `4.3.2-beta.2`. +* Support for auditing entities - automatic management of createdBy, createdDate, lastModifiedBy and lastModifiedDate annotated fields. +* `@GeneratedValue` annotation support for automatic id generation for id fields of `String` type. +* Multi-database configuration support for single cosmos account with multiple databases and multiple cosmos accounts with multiple databases. +* Support for `@Version` annotation on any string field. +* Updated sync APIs return types to `Iterable` types instead of `List`. +* Exposed `CosmosClientBuilder` from Cosmos SDK as spring bean to `@Configuration` class. +* Updated `CosmosConfig` to contain query metrics and response diagnostics processor implementation. +* Support for returning `Optional` data type for single result queries. +#### Renames +* `CosmosDbFactory` to `CosmosFactory`. +* `CosmosDBConfig` to `CosmosConfig`. +* `CosmosDBAccessException` to `CosmosAccessException`. +* `Document` annotation to `Container` annotation. +* `DocumentIndexingPolicy` annotation to `CosmosIndexingPolicy` annotation. +* `DocumentQuery` to `CosmosQuery`. +* application.properties flag `populateQueryMetrics` to `queryMetricsEnabled`. +#### Bugs Fixed +* Scheduling diagnostics logging task to `Parallel` threads to avoid blocking Netty I/O threads. +* Fixed optimistic locking on delete operation. +* Fixed issue with escaping queries for `IN` clause. +* Fixed issue by allowing `long` data type for `@Id`. +* Fixed issue by allowing `boolean`, `long`, `int`, `double` as data types for `@PartitionKey` annotation. +* Fixed `IgnoreCase` & `AllIgnoreCase` keywords for ignore case queries. +* Removed default request unit value of 4000 when creating containers automatically. diff --git a/sdk/spring/azure-spring-data-cosmos/CONTRIBUTING.md b/sdk/spring/azure-spring-data-cosmos/CONTRIBUTING.md new file mode 100644 index 000000000000..fd033030498b --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing +This instruction is guideline for building and code contribution. + +## Prequisites +- JDK 1.8 and above +- [Maven](https://maven.apache.org/) 3.0 and above + +## Build from source +To build the project, run maven commands. + +```bash +git clone https://github.com/Azure/azure-sdk-for-java.git +cd sdk/spring/azure-spring-data-cosmos +mvn clean install +``` + +## Test +There're integration tests on azure and on emulator to trigger integration test execution against Azure Cosmos DB and against [Azure Cosmos DB Emulator](https://docs.microsoft.com/azure/cosmos-db/local-emulator), you need to follow the link to setup emulator before test execution. + +- Run unit tests +```bash +mvn clean install -Dgpg.skip +``` + +- Run integration tests + - on Azure + >**NOTE** Please note that integration test against Azure requires Azure Cosmos DB Document API and will automatically create a Cosmos database in your Azure subscription, then there will be **Azure usage fee.** + + Integration tests will require a Azure Subscription. If you don't already have an Azure subscription, you can activate your [MSDN subscriber benefits](https://azure.microsoft.com/pricing/member-offers/msdn-benefits-details/) or sign up for a [free Azure account](https://azure.microsoft.com/free/). + + 1. Create an Azure Cosmos DB on Azure. + - Go to [Azure portal](https://portal.azure.com/) and click +New. + - Click Databases, and then click Azure Cosmos DB to create your database. + - Navigate to the database you have created, and click Access keys and copy your URI and access keys for your database. + + 2. Set environment variables ACCOUNT_HOST, ACCOUNT_KEY and SECONDARY_ACCOUNT_KEY, where value of them are Cosmos account URI, primary key and secondary key. + + `azure-spring-data-cosmos-core` also support multiple database configuration. So set the second group environment variables NEW_ACCOUNT_HOST, NEW_ACCOUNT_KEY and NEW_SECONDARY_ACCOUNT_KEY, the two group environment variables can be same. + 3. Run maven command with `integration-test-azure` profile. + + ```bash + set ACCOUNT_HOST=your-cosmos-account-uri + set ACCOUNT_KEY=your-cosmos-account-primary-key + set SECONDARY_ACCOUNT_KEY=your-cosmos-account-secondary-key + + set NEW_ACCOUNT_HOST=your-cosmos-account-uri + set NEW_ACCOUNT_KEY=your-cosmos-account-primary-key + set NEW_SECONDARY_ACCOUNT_KEY=your-cosmos-account-secondary-key + mvnw -P integration-test-azure clean install + ``` + + - on Emulator + + Setup Azure Cosmos DB Emulator by following [this instruction](https://docs.microsoft.com/azure/cosmos-db/local-emulator), and set associated environment variables. Then run test with: + ```bash + mvnw -P integration-test-emulator install + ``` + + +- Skip tests execution +```bash +mvn clean install -Dgpg.skip-DskipTests +``` + +## Version management +Developing version naming convention is like `0.1.2-beta.1`. Release version naming convention is like `0.1.2`. + +## Contribute to code +Contribution is welcome. Please follow [this instruction](https://github.com/Azure/azure-sdk-for-java/blob/main/CONTRIBUTING.md) to contribute code. diff --git a/sdk/spring/azure-spring-data-cosmos/README.md b/sdk/spring/azure-spring-data-cosmos/README.md new file mode 100644 index 000000000000..7485e0dc6a28 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/README.md @@ -0,0 +1,1055 @@ +# Azure Spring Data Cosmos client library for Java + +**Azure Spring Data Cosmos** provides Spring Data support for Azure Cosmos DB using the [SQL API][sql_api_query], based on Spring Data framework. +[Azure Cosmos DB][cosmos_introduction] is a globally-distributed database service which allows developers to work with data using a variety of standard APIs, such as SQL, MongoDB, Cassandra, Graph, and Table. + +## Spring Boot Support Policy +This project supports multiple [Spring Boot Versions][spring_version_mapping]. For complete list of currently supported versions, please visit our [Spring Version Mapping][spring_version_mapping]. + +Spring Boot releases are marked as "End of Life" when they are no longer supported or released in any form. If you are running an EOL version, you should upgrade as soon as possible. + +Please note that a version can be out of support before it is marked as "End of Life". During this time you should only expect releases for critical bugs or security issues. + +For more information on Spring Boot supported versions, please visit [Spring Boot Supported Versions][spring_boot_supported_versions]. + +## Spring Boot Version Support +Maven users can inherit from the `spring-boot-starter-parent` project to obtain a dependency management section to let Spring manage the versions for dependencies. + +```xml + + + org.springframework.boot + spring-boot-starter-parent + ${spring.boot.version} + +``` +With that setup, you can also override individual dependencies by overriding a property in your own project. For instance, to upgrade to another Spring Data release train you’d add the following to your pom.xml. +```xml + + ${spring.data.version} + +``` +If you don’t want to use the `spring-boot-starter-parent`, you can still keep the benefit of the dependency management by using a `scope=import` dependency: +```xml + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + +``` + +## Which Version of Azure Spring Data Cosmos Should I Use + +Mapping from **Spring Boot** / **Spring Cloud** version to **Azure Spring Data Cosmos** versions + +| Spring Boot version | Spring Cloud version | Azure Spring Data Cosmos versions | +|-----------------------|------------------------|---------------------------------| +| 2.7.x | 2021.0.x | 3.23.0 and above | +| 2.6.x | 2021.0.x | 3.15.0 - 3.22.0 | +| 2.5.x | 2020.0.x | 3.8.0 - 3.14.0 | +| 2.4.x | 2020.0.x | 3.5.0 - 3.7.0 | + +### I'm Using Spring Boot Version X +If you are using **Spring Boot** in your project, you can find related **Azure Spring Data Cosmos** versions from above table. For example: if you are using **Spring Boot** 2.7.x, you should use **Azure Spring Data Cosmos** versions 3.23.0 and above. + +### I'm Using Spring Cloud Version Y +If you are using **Spring Cloud** in your project, you can also find related **Azure Spring Data Cosmos** versions from above table. For example, if you are using **Spring Cloud** 2021.0.x, you should use **Azure Spring Data Cosmos** versions 3.23.0 and above. + +## Spring Data Version Support +This project supports `spring-data-commons 2.7.x` versions. + +The above setup does not allow you to override individual dependencies using a property as explained above. To achieve the same result, you’d need to add an entry in the dependencyManagement of your project before the `spring-boot-dependencies` entry. For instance, to upgrade to another Spring Data release train you’d add the following to your pom.xml. +```xml + + + + + org.springframework.data + spring-data-releasetrain + ${spring.data.version} + import + pom + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + +``` +>**Note:** Replace the ${spring.boot.version} and ${spring.data.version} with the versions of Spring Boot and Spring Data you want to use in your project. + +## Getting started + +### Include the package +If you are using Maven, add the following dependency. + +[//]: # ({x-version-update-start;com.azure:azure-spring-data-cosmos;current}) +```xml + + com.azure + azure-spring-data-cosmos + 3.34.0 + +``` +[//]: # ({x-version-update-end}) + +### Prerequisites + +- [Java Development Kit (JDK)][jdk_link], version 8 or later. +- An active Azure account. If you don't have one, you can sign up for a [free account][azure_subscription]. Alternatively, you can use the [Azure Cosmos DB Emulator][local_emulator] for development and testing. As emulator https certificate is self signed, you need to import its certificate to java trusted cert store, [explained here][local_emulator_export_ssl_certificates] +- (Optional) SLF4J is a logging facade. +- (Optional) [SLF4J binding](https://www.slf4j.org/manual.html) is used to associate a specific logging framework with SLF4J. +- (Optional) [Maven][maven_link] + +SLF4J is only needed if you plan to use logging, please also download an SLF4J binding which will link the SLF4J API with the logging implementation of your choice. See the [SLF4J user manual](https://www.slf4j.org/manual.html) for more information. + +### Setup Configuration Class +- In order to set up configuration class, you'll need to extend `AbstractCosmosConfiguration` + +- Azure-spring-data-cosmos also supports `Response Diagnostics String`, `Query Metrics` and `Max Degree of Parallelism`. +Set `queryMetricsEnabled` flag to true in application.properties to enable query metrics. +In addition to setting the flag, implement `ResponseDiagnosticsProcessor` to log diagnostics information. +Set `maxDegreeOfParallelism` flag to an integer in application.properties to allow parallel processing; setting the value to -1 will lead to the SDK deciding the optimal value. +Set `maxBufferedItemCount` flag to an integer in application.properties to allow the user to set the max number of items that can be buffered during parallel query execution; if set to less than 0, the system automatically decides the number of items to buffer. +NOTE: Setting this to a very high value can result in high memory consumption. +Set `responseContinuationTokenLimitInKb` flag to an integer in application.properties to allow the user to limit the length of the continuation token in the query response. The continuation token contains both required and optional fields. The required fields are necessary for resuming the execution from where it was stoped. The optional fields may contain serialized index lookup work that was done but not yet utilized. This avoids redoing the work again in subsequent continuations and hence improve the query performance. Setting the maximum continuation size to 1KB, the Azure Cosmos DB service will only serialize required fields. Starting from 2KB, the Azure Cosmos DB service would serialize as much as it could fit till it reaches the maximum specified size. + +```java readme-sample-AppConfiguration +@Configuration +@EnableCosmosRepositories +public class AppConfiguration extends AbstractCosmosConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfiguration.class); + + @Value("${azure.cosmos.uri}") + private String uri; + + @Value("${azure.cosmos.key}") + private String key; + + @Value("${azure.cosmos.secondaryKey}") + private String secondaryKey; + + @Value("${azure.cosmos.database}") + private String dbName; + + @Value("${azure.cosmos.queryMetricsEnabled}") + private boolean queryMetricsEnabled; + + @Value("${azure.cosmos.maxDegreeOfParallelism}") + private int maxDegreeOfParallelism; + + @Value("${azure.cosmos.maxBufferedItemCount}") + private int maxBufferedItemCount; + + @Value("${azure.cosmos.responseContinuationTokenLimitInKb}") + private int responseContinuationTokenLimitInKb; + + private AzureKeyCredential azureKeyCredential; + + @Bean + public CosmosClientBuilder getCosmosClientBuilder() { + this.azureKeyCredential = new AzureKeyCredential(key); + DirectConnectionConfig directConnectionConfig = new DirectConnectionConfig(); + GatewayConnectionConfig gatewayConnectionConfig = new GatewayConnectionConfig(); + return new CosmosClientBuilder() + .endpoint(uri) + .credential(azureKeyCredential) + .directMode(directConnectionConfig, gatewayConnectionConfig); + } + + @Override + public CosmosConfig cosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(queryMetricsEnabled) + .maxDegreeOfParallelism(maxDegreeOfParallelism) + .maxBufferedItemCount(maxBufferedItemCount) + .responseContinuationTokenLimitInKb(responseContinuationTokenLimitInKb) + .responseDiagnosticsProcessor(new ResponseDiagnosticsProcessorImplementation()) + .build(); + } + + public void switchToSecondaryKey() { + this.azureKeyCredential.update(secondaryKey); + } + + @Override + protected String getDatabaseName() { + return "testdb"; + } + + private static class ResponseDiagnosticsProcessorImplementation implements ResponseDiagnosticsProcessor { + + @Override + public void processResponseDiagnostics(@Nullable ResponseDiagnostics responseDiagnostics) { + LOGGER.info("Response Diagnostics {}", responseDiagnostics); + } + } + +} +``` +### Customizing Configuration +You can customize `DirectConnectionConfig` or `GatewayConnectionConfig` or both and provide them to `CosmosClientBuilder` bean to customize `CosmosAsyncClient` + +```java readme-sample-AppConfigurationCodeSnippet +@Bean +public CosmosClientBuilder getCosmosClientBuilder() { + + DirectConnectionConfig directConnectionConfig = new DirectConnectionConfig(); + GatewayConnectionConfig gatewayConnectionConfig = new GatewayConnectionConfig(); + return new CosmosClientBuilder() + .endpoint(uri) + .directMode(directConnectionConfig, gatewayConnectionConfig); +} + +@Override +public CosmosConfig cosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(queryMetricsEnabled) + .maxDegreeOfParallelism(maxDegreeOfParallelism) + .maxBufferedItemCount(maxBufferedItemCount) + .responseContinuationTokenLimitInKb(responseContinuationTokenLimitInKb) + .responseDiagnosticsProcessor(new ResponseDiagnosticsProcessorImplementation()) + .build(); +} +``` + +By default, `@EnableCosmosRepositories` will scan the current package for any interfaces that extend one of Spring Data's repository interfaces. +Use it to annotate your Configuration class to scan a different root package by `@EnableCosmosRepositories(basePackageClass=UserRepository.class)` if your project layout has multiple projects. + +#### Using database provisioned throughput + +Cosmos supports both [container](https://docs.microsoft.com/azure/cosmos-db/sql/how-to-provision-container-throughput) +and [database](https://docs.microsoft.com/azure/cosmos-db/sql/how-to-provision-database-throughput) provisioned +throughput. By default, spring-data-cosmos will provision throughput for each container created. If you prefer +to share throughput between containers, you can enable database provisioned throughput via CosmosConfig. + +```java +@Override +public CosmosConfig cosmosConfig() { + int autoscale = false; + int initialRequestUnits = 400; + return CosmosConfig.builder() + .enableDatabaseThroughput(autoscale, initialRequestUnits) + .build(); +} +``` + +### Define an entity +- Define a simple entity as item in Azure Cosmos DB. + +- You can define entities by adding the `@Container` annotation and specifying properties related to the container, such as the container name, request units (RUs), time to live, and auto-create container. + +- Containers will be created automatically unless you don't want them to. Set `autoCreateContainer` to false in `@Container` annotation to disable auto creation of containers. + +- Note: By default request units assigned to newly created containers is 400. Specify different ru value to customize request units for the container created by the SDK (minimum RU value is 400). +```java readme-sample-User +@Container(containerName = "myContainer", ru = "400") +public class User { + private String id; + private String firstName; + + + @PartitionKey + private String lastName; + + 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); + } + + 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; + } +} +``` +- `id` field will be used as Item id in Azure Cosmos DB. If you want use another field like `firstName` as item `id`, just annotate that field with `@Id` annotation. + +- Annotation `@Container(containerName="myContainer")` specifies container name in Azure Cosmos DB. +- Annotation `@PartitionKey` on `lastName` field specifies this field as partition key in Azure Cosmos DB. + +#### Creating Containers with autoscale throughput +- Annotation `autoScale` field specifies container to be created with autoscale throughput if set to true. Default is false, which means containers are created with manual throughput. +- Read more about autoscale throughput [here][autoscale-throughput] +```java readme-sample-UserSample +@Container(containerName = "myContainer", autoScale = true, ru = "4000") +public class UserSample { + @Id + private String emailAddress; + +} +``` +#### Nested Partition Key support + +- Azure Spring Data Cosmos supports nested partition key. To add nested partition key, use `partitionKeyPath` field in `@Container` annotation. +- `partitionKeyPath` should only be used to support nested partition key path. For general partition key support, use the `@PartitionKey` annotation. +- By default `@PartitionKey` annotation will take precedence, unless not specified. +- Below example shows how to properly use Nested Partition key feature. + +```java readme-sample-NestedPartitionKeyEntitySample +@Container(containerName = "nested-partition-key", partitionKeyPath = "/nestedEntitySample/nestedPartitionKey") +public class NestedPartitionKeyEntitySample { + + private NestedEntitySample nestedEntitySample; +} +``` + +```java readme-sample-NestedEntitySample +public class NestedEntitySample { + private String nestedPartitionKey; +} +``` + +### Create repositories +Extends CosmosRepository interface, which provides Spring Data repository support. + +```java readme-sample-UserRepository +@Repository +public interface UserRepository extends CosmosRepository { + Iterable findByFirstName(String firstName); + long countByFirstName(String firstName); + User findOne(String id, String lastName); +} +``` + +- `findByFirstName` method is custom query method, it will find items per firstName. + +#### Query Plan Caching +Spring repository query APIs like `findByFirstName(String firstName)` where `firstName` is the partition or annotated queries containing partition key will result in lower query execution time because of query plan caching. Currently, query plan caching is only supported for query methods targeting a single partition. + +#### QueryAnnotation : Using annotated queries in repositories +Azure spring data cosmos supports specifying annotated queries in the repositories using `@Query`. +- Examples for annotated queries in synchronous CosmosRepository: + +```java readme-sample-AnnotatedQueriesUserRepositoryCodeSnippet +public interface AnnotatedQueriesUserRepositoryCodeSnippet extends CosmosRepository { + @Query("select * from c where c.firstName = @firstName and c.lastName = @lastName") + List getUsersByFirstNameAndLastName(@Param("firstName") String firstName, @Param("lastName") String lastName); + + @Query("select * from c offset @offset limit @limit") + List getUsersWithOffsetLimit(@Param("offset") int offset, @Param("limit") int limit); + + @Query("select value count(1) from c where c.firstName = @firstName") + long getNumberOfUsersWithFirstName(@Param("firstName") String firstName); +} +``` + +- Examples for annotated queries in ReactiveCosmosRepository. + +```java readme-sample-AnnotatedQueriesUserReactiveRepositoryCodeSnippet +public interface AnnotatedQueriesUserReactiveRepositoryCodeSnippet extends ReactiveCosmosRepository { + @Query("select * from c where c.firstName = @firstName and c.lastName = @lastName") + Flux getUsersByTitleAndValue(@Param("firstName") int firstName, @Param("lastName") String lastName); + + @Query("select * from c offset @offset limit @limit") + Flux getUsersWithOffsetLimit(@Param("offset") int offset, @Param("limit") int limit); + + @Query("select count(c.id) as num_ids, c.lastName from c group by c.lastName") + Flux getCoursesGroupByDepartment(); + + @Query("select value count(1) from c where c.lastName = @lastName") + Mono getNumberOfUsersWithLastName(@Param("lastName") String lastName); +} +``` + +The queries that are specified in the annotation are same as the cosmos queries. +Please refer to the following articles for more information on sql queries in cosmos + - [Sql API Query Getting Started] [sql_queries_getting_started] + - [Sql API Query Tutorial] [sql_queries_in_cosmos] + +### Create an Application class +Here create an application class with all the components + +```java readme-sample-SampleApplication +@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) { + + 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()); + + // Switch to secondary key + UserRepositoryConfiguration bean = + applicationContext.getBean(UserRepositoryConfiguration.class); + bean.switchToSecondaryKey(); + + // Now repository will use secondary key + repository.save(testUser); + + } +} +``` +- Autowire UserRepository interface, to perform operations like save, delete, find, etc. +- Spring Data Azure Cosmos DB uses the `CosmosTemplate` and `ReactiveCosmosTemplate` to execute the queries behind *find*, *save* methods. You can use the template yourself for more complex queries. + +## Key concepts + +### CrudRepository and ReactiveCrudRepository +- Azure Spring Data Cosmos supports ReactiveCrudRepository and CrudRepository which provides basic CRUD functionality + - save + - findAll + - findOne by Id + - deleteAll + - delete by Id + - delete entity + +### Spring Data Annotations +#### Spring Data [@Id annotation][spring_data_commons_id_annotation] + There are 2 ways to map a field in domain class to `id` field of Azure Cosmos DB Item. + - annotate a field in domain class with `@Id`, this field will be mapped to Item `id` in Cosmos DB. + - set name of this field to `id`, this field will be mapped to Item `id` in Azure Cosmos DB. +#### Id auto generation + - Supports auto generation of string type UUIDs using the @GeneratedValue annotation. The id field of an entity with a string + type id can be annotated with `@GeneratedValue` to automatically generate a random UUID prior to insertion. + ```java readme-sample-GeneratedIdEntity + public class GeneratedIdEntity { + + @Id + @GeneratedValue + private String id; + + } + ``` +#### SpEL Expression and Custom Container Name. + - By default, container name will be class name of user domain class. To customize it, add the `@Container(containerName="myCustomContainerName")` annotation to the domain class. The container field also supports SpEL expressions (eg. `container = "${dynamic.container.name}"` or `container = "#{@someBean.getContainerName()}"`) in order to provide container names programmatically/via configuration properties. + - In order for SpEL expressions to work properly, you need to add `@DependsOn("expressionResolver")` on top of Spring Application class. +```java +@SpringBootApplication +@DependsOn("expressionResolver") +public class SampleApplication { + +} +``` +#### Indexing Policy +- By default, IndexingPolicy will be set by Azure Portal Service. To customize it add annotation `@CosmosIndexingPolicy` to domain class. This annotation has 5 attributes to customize, see following: +```java readme-sample-CosmosIndexingPolicyCodeSnippet +// Indicate if indexing policy use automatic or not +// Default value is true +boolean automatic() default Constants.DEFAULT_INDEXING_POLICY_AUTOMATIC; + +// Indexing policy mode, option Consistent. +IndexingMode mode() default IndexingMode.CONSISTENT; + +// Included paths for indexing +String[] includePaths() default {}; + +// Excluded paths for indexing +String[] excludePaths() default {}; +``` +#### Unique Key Policy + - Azure Spring Data Cosmos supports setting `UniqueKeyPolicy` on container by adding the annotation `@CosmosUniqueKeyPolicy` to domain class. This annotation has the following attributes: +```java readme-sample-CosmosUniqueKeyPolicyCodeSnippet +@Container +@CosmosUniqueKeyPolicy(uniqueKeys = { + @CosmosUniqueKey(paths = {"/lastName", "/zipCode"}), + @CosmosUniqueKey(paths = {"/city"}) +}) +public class CosmosUniqueKeyPolicyCodeSnippet { + + @Id + String id; + + @PartitionKey + String firstName; + + String lastName; + String zipCode; + String city; +} +``` +### Azure Cosmos DB Partition +- Azure-spring-data-cosmos supports [Azure Cosmos DB partition][azure_cosmos_db_partition]. +- To specify a field of domain class to be partition key field, just annotate it with `@PartitionKey`. +- When you perform CRUD operation, specify your partition value. +- For more sample on partition CRUD, please refer [test here][address_repository_it_test] + +### Optimistic Locking +- Azure-spring-data-cosmos supports Optimistic Locking for specific containers, which means upserts/deletes by item will fail with an exception in case the item is modified by another process in the meanwhile. +- To enable Optimistic Locking for a container, just create a string `_etag` field and mark it with the `@Version` annotation. See the following: + +```java readme-sample-MyItem +@Container(containerName = "myContainer") +public class MyItem { + String id; + String data; + @Version + String _etag; +} +``` +- Read more about Optimistic Locking [here](https://docs.microsoft.com/azure/cosmos-db/sql/database-transactions-optimistic-concurrency#optimistic-concurrency-control) + +### Spring Data custom query, pageable and sorting +- Azure-spring-data-cosmos supports [spring data custom queries][spring_data_custom_query] +- Example, find operation, e.g., `findByAFieldAndBField` +- Supports [Spring Data Pageable, Slice 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 items less than or equal to the requested size. + - Due to this variable number of returned items in every iteration, user should not rely on the totalPageSize, and instead iterating over pageable should be done in this way. +```java readme-sample-findAllWithPageSize +private List findAllWithPageSize(int pageSize) { + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, pageSize, null); + Page page = repository.findAll(pageRequest); + List pageContent = page.getContent(); + while (page.hasNext()) { + Pageable nextPageable = page.nextPageable(); + page = repository.findAll(nextPageable); + pageContent = page.getContent(); + } + return pageContent; +} +``` + +```java readme-sample-SliceQueriesUserRepository +public interface SliceQueriesUserRepository extends CosmosRepository { + @Query("select * from c where c.lastName = @lastName") + Slice getUsersByLastName(@Param("lastName") String lastName, Pageable pageable); +} +``` + +```java readme-sample-getUsersByLastName +private List getUsersByLastName(String lastName, int pageSize) { + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, pageSize, null); + Slice slice = repository.getUsersByLastName(lastName, pageRequest); + List content = slice.getContent(); + while (slice.hasNext()) { + Pageable nextPageable = slice.nextPageable(); + slice = repository.getUsersByLastName(lastName, nextPageable); + content.addAll(slice.getContent()); + } + return content; +} +``` + +### Spring Boot Starter Data Rest +- Azure-spring-data-cosmos supports [spring-boot-starter-data-rest](https://spring.io/projects/spring-data-rest). +- Supports List and nested type in domain class. +- Configurable ObjectMapper bean with unique name `cosmosObjectMapper`, only configure customized ObjectMapper if you really need to. e.g., +```java readme-sample-objectMapper +@Bean(name = "cosmosObjectMapper") +public ObjectMapper objectMapper() { + return new ObjectMapper(); // Do configuration to the ObjectMapper if required +} +``` + +### Auditing +- Azure-spring-data-cosmos supports auditing fields on database entities using standard spring-data annotations. +- This feature can be enabled by adding `@EnableCosmosAuditing` annotation to your application configuration. +- Entities can annotate fields using `@CreatedBy`, `@CreatedDate`, `@LastModifiedBy` and `@LastModifiedDate`. These fields will be updated automatically. +```java readme-sample-AuditableUser +@Container(containerName = "myContainer") +public class AuditableUser { + private String id; + private String firstName; + @CreatedBy + private String createdBy; + @CreatedDate + private OffsetDateTime createdDate; + @LastModifiedBy + private String lastModifiedBy; + @LastModifiedDate + private OffsetDateTime lastModifiedByDate; +} +``` + +### Multi-database configuration +- Azure-spring-data-cosmos supports multi-database configuration, including "multiple database accounts" and "single account, with multiple databases". + +#### Multi-database accounts +The example uses the `application.properties` file +```properties +# primary account cosmos config +azure.cosmos.primary.uri=your-primary-cosmosDb-uri +azure.cosmos.primary.key=your-primary-cosmosDb-key +azure.cosmos.primary.secondaryKey=your-primary-cosmosDb-secondary-key +azure.cosmos.primary.database=your-primary-cosmosDb-dbName +azure.cosmos.primary.populateQueryMetrics=if-populate-query-metrics + +# secondary account cosmos config +azure.cosmos.secondary.uri=your-secondary-cosmosDb-uri +azure.cosmos.secondary.key=your-secondary-cosmosDb-key +azure.cosmos.secondary.secondaryKey=your-secondary-cosmosDb-secondary-key +azure.cosmos.secondary.database=your-secondary-cosmosDb-dbName +azure.cosmos.secondary.populateQueryMetrics=if-populate-query-metrics +``` + +- The [Entity](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-data-cosmos#define-an-entity) and [Repository](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-data-cosmos#create-repositories) definition is similar as above. You can put different database entities into different packages. + +- The `@EnableReactiveCosmosRepositories` or `@EnableCosmosRepositories` support user-define the cosmos template, use `reactiveCosmosTemplateRef` or `cosmosTemplateRef` to config the name of the `ReactiveCosmosTemplate` or `CosmosTemplate` bean to be used with the repositories detected. +- If you have multiple cosmos database accounts, you can define multiple `CosmosAsyncClient`. If the single cosmos account has multiple databases, you can use the same `CosmosAsyncClient` to initialize the cosmos template. + +```java +@Configuration +@EnableReactiveCosmosRepositories(basePackages = "com.azure.spring.sample.cosmos.multi.database.multiple.account.repository", + reactiveCosmosTemplateRef = "primaryDatabaseTemplate") +public class PrimaryDatasourceConfiguration extends AbstractCosmosConfiguration{ + + private static final String PRIMARY_DATABASE = "primary_database"; + + @Bean + @ConfigurationProperties(prefix = "azure.cosmos.primary") + public CosmosProperties primary() { + return new CosmosProperties(); + } + + @Bean + public CosmosClientBuilder primaryClientBuilder(@Qualifier("primary") CosmosProperties primaryProperties) { + return new CosmosClientBuilder() + .key(primaryProperties.getKey()) + .endpoint(primaryProperties.getUri()); + } + + @Bean + public ReactiveCosmosTemplate primaryDatabaseTemplate(CosmosAsyncClient cosmosAsyncClient, + CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter) { + return new ReactiveCosmosTemplate(cosmosAsyncClient, PRIMARY_DATABASE, cosmosConfig, mappingCosmosConverter); + } + + @Override + protected String getDatabaseName() { + return PRIMARY_DATABASE; + } +} +``` + +```java +@Configuration +@EnableCosmosRepositories(cosmosTemplateRef = "secondaryDatabaseTemplate") +public class SecondaryDatasourceConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(SecondaryDatasourceConfiguration.class); + public static final String SECONDARY_DATABASE = "secondary_database"; + + @Bean + @ConfigurationProperties(prefix = "azure.cosmos.secondary") + public CosmosProperties secondary() { + return new CosmosProperties(); + } + + @Bean("secondaryCosmosClient") + public CosmosAsyncClient getCosmosAsyncClient(@Qualifier("secondary") CosmosProperties secondaryProperties) { + return CosmosFactory.createCosmosAsyncClient(new CosmosClientBuilder() + .key(secondaryProperties.getKey()) + .endpoint(secondaryProperties.getUri())); + } + + @Bean("secondaryCosmosConfig") + public CosmosConfig getCosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(true) + .maxDegreeOfParallelism(0) + .maxBufferedItemCount(0) + .responseContinuationTokenLimitInKb(0) + .responseDiagnosticsProcessor(new ResponseDiagnosticsProcessorImplementation()) + .build(); + } + + @Bean + public CosmosTemplate secondaryDatabaseTemplate(@Qualifier("secondaryCosmosClient") CosmosAsyncClient client, + @Qualifier("secondaryCosmosConfig") CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter) { + return new CosmosTemplate(client, SECONDARY_DATABASE, cosmosConfig, mappingCosmosConverter); + } + + private static class ResponseDiagnosticsProcessorImplementation implements ResponseDiagnosticsProcessor { + + @Override + public void processResponseDiagnostics(@Nullable ResponseDiagnostics responseDiagnostics) { + LOGGER.info("Response Diagnostics {}", responseDiagnostics); + } + } +} +``` + +- In the above example, we have two cosmos account. You can create the `CosmosAsyncClient` like this: + +```java +@Bean("secondaryCosmosClient") +public CosmosAsyncClient getCosmosAsyncClient(@Qualifier("secondary") CosmosProperties secondaryProperties) { + return CosmosFactory.createCosmosAsyncClient(new CosmosClientBuilder() + .key(secondaryProperties.getKey()) + .endpoint(secondaryProperties.getUri())); +} + +@Bean("secondaryCosmosConfig") +public CosmosConfig getCosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(true) + .maxDegreeOfParallelism(0) + .maxBufferedItemCount(0) + .responseContinuationTokenLimitInKb(0) + .responseDiagnosticsProcessor(new ResponseDiagnosticsProcessorImplementation()) + .build(); +} +``` + +- Besides, if you want to define `queryMetricsEnabled`, `ResponseDiagnosticsProcessor`, `maxDegreeOfParallelism`, `maxBufferedItemCount` or `responseContinuationTokenLimitInKb` , you can create the `CosmosConfig` for your cosmos template. + +```java +@Bean("secondaryCosmosConfig") +public CosmosConfig getCosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(true) + .maxDegreeOfParallelism(0) + .maxBufferedItemCount(0) + .responseContinuationTokenLimitInKb(0) + .responseDiagnosticsProcessor(new ResponseDiagnosticsProcessorImplementation()) + .build(); +} +``` + +- Create an Application class + +```java +@SpringBootApplication +public class MultiDatabaseApplication implements CommandLineRunner { + + @Autowired + private CosmosUserRepository cosmosUserRepository; + + @Autowired + private MysqlUserRepository mysqlUserRepository; + + @Autowired + @Qualifier("secondaryDatabaseTemplate") + private CosmosTemplate secondaryDatabaseTemplate; + + @Autowired + @Qualifier("primaryDatabaseTemplate") + private ReactiveCosmosTemplate primaryDatabaseTemplate; + + private final CosmosUser cosmosUser = new CosmosUser("1024", "1024@geek.com", "1k", "Mars"); + private static CosmosEntityInformation userInfo = new CosmosEntityInformation<>(CosmosUser.class); + + public static void main(String[] args) { + SpringApplication.run(MultiDatabaseApplication.class, args); + } + + public void run(String... var1) throws Exception { + + CosmosUser cosmosUserGet = primaryDatabaseTemplate.findById(cosmosUser.getId(), cosmosUser.getClass()).block(); + // Same to this.cosmosUserRepository.findById(cosmosUser.getId()).block(); + MysqlUser mysqlUser = new MysqlUser(cosmosUserGet.getId(), cosmosUserGet.getEmail(), cosmosUserGet.getName(), cosmosUserGet.getAddress()); + mysqlUserRepository.save(mysqlUser); + mysqlUserRepository.findAll().forEach(System.out::println); + CosmosUser secondaryCosmosUserGet = secondaryDatabaseTemplate.findById(CosmosUser.class.getSimpleName(), cosmosUser.getId(), CosmosUser.class); + System.out.println(secondaryCosmosUserGet); + } + + + @PostConstruct + public void setup() { + primaryDatabaseTemplate.createContainerIfNotExists(userInfo).block(); + primaryDatabaseTemplate.insert(CosmosUser.class.getSimpleName(), cosmosUser, new PartitionKey(cosmosUser.getName())).block(); + // Same to this.cosmosUserRepository.save(user).block(); + secondaryDatabaseTemplate.createContainerIfNotExists(userInfo); + secondaryDatabaseTemplate.insert(CosmosUser.class.getSimpleName(), cosmosUser, new PartitionKey(cosmosUser.getName())); + } + + @PreDestroy + public void cleanup() { + primaryDatabaseTemplate.deleteAll(CosmosUser.class.getSimpleName(), CosmosUser.class).block(); + // Same to this.cosmosUserRepository.deleteAll().block(); + secondaryDatabaseTemplate.deleteAll(CosmosUser.class.getSimpleName() , CosmosUser.class); + mysqlUserRepository.deleteAll(); + } +} +``` + +#### Single account with Multi-database +The example uses the `application.properties` file +```properties +azure.cosmos.uri=your-cosmosDb-uri +azure.cosmos.key=your-cosmosDb-key +azure.cosmos.secondary-key=your-cosmosDb-secondary-key +azure.cosmos.database=your-cosmosDb-dbName +azure.cosmos.populate-query-metrics=if-populate-query-metrics +``` + +- The [Entity](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-data-cosmos#define-an-entity) and [Repository](https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-data-cosmos#create-repositories) definition is similar as above. You can put different database entities into different packages. +- You can use `EnableReactiveCosmosRepositories` with different `reactiveCosmosTemplateRef` to define multiple databases in single cosmos account. + +```java +@Configuration +public class DatasourceConfiguration { + + private static final String DATABASE1 = "database1"; + private static final String DATABASE2 = "database2"; + + @Bean + public CosmosProperties cosmosProperties() { + return new CosmosProperties(); + } + + @Bean + public CosmosClientBuilder primaryClientBuilder(CosmosProperties cosmosProperties) { + return new CosmosClientBuilder() + .key(cosmosProperties.getKey()) + .endpoint(cosmosProperties.getUri()); + } + + @EnableReactiveCosmosRepositories(basePackages = "com.azure.spring.sample.cosmos.multi.database.repository1", + reactiveCosmosTemplateRef = "database1Template") + public class Database1Configuration extends AbstractCosmosConfiguration { + + @Bean + public ReactiveCosmosTemplate database1Template(CosmosAsyncClient cosmosAsyncClient, + CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter) { + return new ReactiveCosmosTemplate(cosmosAsyncClient, DATABASE1, cosmosConfig, mappingCosmosConverter); + } + + @Override + protected String getDatabaseName() { + return DATABASE1; + } + } + + @EnableReactiveCosmosRepositories(basePackages = "com.azure.spring.sample.cosmos.multi.database.repository2", + reactiveCosmosTemplateRef = "database2Template") + public class Database2Configuration { + + @Bean + public ReactiveCosmosTemplate database2Template(CosmosAsyncClient cosmosAsyncClient, + CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter) { + return new ReactiveCosmosTemplate(cosmosAsyncClient, DATABASE2, cosmosConfig, mappingCosmosConverter); + } + + } +} +``` + +- Create an Application class + +```java +@SpringBootApplication +public class MultiDatabaseApplication implements CommandLineRunner { + + @Autowired + private User1Repository user1Repository; + + @Autowired + @Qualifier("database1Template") + private ReactiveCosmosTemplate database1Template; + + @Autowired + @Qualifier("database2Template") + private ReactiveCosmosTemplate database2Template; + + private final User1 user1 = new User1("1024", "1024@geek.com", "1k", "Mars"); + private static CosmosEntityInformation user1Info = new CosmosEntityInformation<>(User1.class); + + private final User2 user2 = new User2("2048", "2048@geek.com", "2k", "Mars"); + private static CosmosEntityInformation user2Info = new CosmosEntityInformation<>(User2.class); + + + public static void main(String[] args) { + SpringApplication.run(MultiDatabaseApplication.class, args); + } + + public void run(String... var1) throws Exception { + + User1 database1UserGet = database1Template.findById(User1.class.getSimpleName(), user1.getId(), User1.class).block(); + // Same to userRepository1.findById(user.getId()).block() + System.out.println(database1UserGet); + User2 database2UserGet = database2Template.findById(User2.class.getSimpleName(), user2.getId(), User2.class).block(); + System.out.println(database2UserGet); + } + + @PostConstruct + public void setup() { + database1Template.createContainerIfNotExists(user1Info).block(); + database1Template.insert(User1.class.getSimpleName(), user1, new PartitionKey(user1.getName())).block(); + // Same to this.userRepository1.save(user).block(); + database2Template.createContainerIfNotExists(user2Info).block(); + database2Template.insert(User2.class.getSimpleName(), user2, new PartitionKey(user2.getName())).block(); + } + + @PreDestroy + public void cleanup() { + database1Template.deleteAll(User1.class.getSimpleName(), User1.class).block(); + // Same to this.userRepository1.deleteAll().block(); + database2Template.deleteAll(User2.class.getSimpleName(), User2.class).block(); + } +} +``` + +### Multi-Tenancy at the Database Level +- Azure-spring-data-cosmos supports multi-tenancy at the database level configuration by extending `CosmosFactory` and overriding the getDatabaseName() function. +```java readme-sample-MultiTenantDBCosmosFactory +public class MultiTenantDBCosmosFactory extends CosmosFactory { + + private String tenantId; + + /** + * Validate config and initialization + * + * @param cosmosAsyncClient cosmosAsyncClient + * @param databaseName databaseName + */ + public MultiTenantDBCosmosFactory(CosmosAsyncClient cosmosAsyncClient, String databaseName) { + super(cosmosAsyncClient, databaseName); + + this.tenantId = databaseName; + } + + @Override + public String getDatabaseName() { + return this.getCosmosAsyncClient().getDatabase(this.tenantId).toString(); + } +} +``` + +## Beta version package + +Beta version built from `main` branch are available, you can refer to the [instruction](https://github.com/Azure/azure-sdk-for-java/blob/main/CONTRIBUTING.md#nightly-package-builds) to use beta version packages. + +## Troubleshooting + +### General + +If you encounter any bug, please file an issue [here](https://github.com/Azure/azure-sdk-for-java/issues/new). + +To suggest a new feature or changes that could be made, file an issue the same way you would for a bug. + +### Enable Client Logging +- Azure-spring-data-cosmos uses SLF4j as the logging facade that supports logging into popular logging frameworks such as log4j and logback. +For example, if you want to use spring logback as logging framework, add the following xml to resources folder. + +```xml + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + +``` + +## Examples +- Please refer to [sample project here][samples]. + +### Multi-database accounts +- Please refer to [Multi-database sample project][sample-for-multi-database]. + +### Single account with Multi-database +- Please refer to [Single account with Multi-database sample project][sample-for-multi-database-single-account]. + +## Next steps +- Read more about azure spring data cosmos [here][azure_spring_data_cosmos_docs]. +- [Read more about Azure CosmosDB Service][cosmos_docs] + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +[Contributor License Agreement (CLA)][cla] declaring that you have the right to, and actually do, grant us the rights +to use your contribution. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate +the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to +do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct][coc]. For more information see the [Code of Conduct FAQ][coc_faq] +or contact [opencode@microsoft.com][coc_contact] with any additional questions or comments. + + +[source_code]: src +[cosmos_introduction]: https://docs.microsoft.com/azure/cosmos-db/ +[cosmos_docs]: https://docs.microsoft.com/azure/cosmos-db/introduction +[jdk]: https://docs.microsoft.com/java/azure/jdk/?view=azure-java-stable +[maven]: https://maven.apache.org/ +[cla]: https://cla.microsoft.com +[coc]: https://opensource.microsoft.com/codeofconduct/ +[coc_faq]: https://opensource.microsoft.com/codeofconduct/faq/ +[coc_contact]: mailto:opencode@microsoft.com +[azure_subscription]: https://azure.microsoft.com/free/ +[samples]: https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos +[sample-for-multi-database]: https://github.com/Azure-Samples/azure-spring-boot-samples/tree/spring-cloud-azure_v4.3.0/cosmos/azure-spring-data-cosmos/cosmos-multi-database-multi-account +[sample-for-multi-database-single-account]: https://github.com/Azure-Samples/azure-spring-boot-samples/tree/spring-cloud-azure_v4.3.0/cosmos/azure-spring-data-cosmos/cosmos-multi-database-single-account +[sql_api_query]: https://docs.microsoft.com/azure/cosmos-db/sql-api-sql-query +[local_emulator]: https://docs.microsoft.com/azure/cosmos-db/local-emulator +[local_emulator_export_ssl_certificates]: https://docs.microsoft.com/azure/cosmos-db/local-emulator-export-ssl-certificates +[spring_data_commons_id_annotation]: https://github.com/spring-projects/spring-data-commons/blob/main/src/main/java/org/springframework/data/annotation/Id.java +[azure_cosmos_db_partition]: https://docs.microsoft.com/azure/cosmos-db/partition-data +[address_repository_it_test]: https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/spring/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/AddressRepositoryIT.java +[azure_spring_data_cosmos_docs]: https://docs.microsoft.com/azure/cosmos-db/sql-api-sdk-java-spring-v3 +[spring_data_custom_query]: https://docs.spring.io/spring-data/commons/docs/current/reference/html/#repositories.query-methods.details +[sql_queries_in_cosmos]: https://docs.microsoft.com/azure/cosmos-db/tutorial-query-sql-api +[sql_queries_getting_started]: https://docs.microsoft.com/azure/cosmos-db/sql-query-getting-started +[jdk_link]: https://docs.microsoft.com/java/azure/jdk/ +[maven_link]: https://maven.apache.org/ +[autoscale-throughput]: https://docs.microsoft.com/azure/cosmos-db/provision-throughput-autoscale +[spring_version_mapping]: https://aka.ms/spring/versions +[spring_boot_supported_versions]: https://github.com/spring-projects/spring-boot/wiki/Supported-Versions + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-java%2Fsdk%2Fcosmos%2F%2Fazure-spring-data-cosmos%2FREADME.png) diff --git a/sdk/spring/azure-spring-data-cosmos/pom.xml b/sdk/spring/azure-spring-data-cosmos/pom.xml new file mode 100644 index 000000000000..54b43242c22e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/pom.xml @@ -0,0 +1,224 @@ + + + 4.0.0 + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.azure + azure-spring-data-cosmos + 3.35.0-beta.1 + jar + Spring Data for Azure Cosmos DB SQL API + Spring Data for Azure Cosmos DB SQL API + https://github.com/Azure/azure-sdk-for-java/tree/master/sdk/spring/azure-spring-data-cosmos + + + MM-dd-HH-mm-ss + 0.17 + 0.18 + + + + --add-opens com.azure.spring.data.cosmos/com.azure.spring.data.cosmos.common=spring.core + --add-opens com.azure.spring.data.cosmos/com.azure.spring.data.cosmos.core=ALL-UNNAMED + --add-opens com.azure.spring.data.cosmos/com.azure.spring.data.cosmos.core.convert=com.fasterxml.jackson.databind + --add-opens com.azure.spring.data.cosmos/com.azure.spring.data.cosmos.core.converter=ALL-UNNAMED + --add-opens com.azure.spring.data.cosmos/com.azure.spring.data.cosmos.core.mapping=spring.core + --add-opens com.azure.spring.data.cosmos/com.azure.spring.data.cosmos.repository=ALL-UNNAMED + --add-opens com.azure.spring.data.cosmos/com.azure.spring.data.cosmos.repository.query=ALL-UNNAMED + --add-opens com.azure.spring.data.cosmos/com.azure.spring.data.cosmos.repository.support=ALL-UNNAMED,spring.core + --add-opens com.azure.spring.data.cosmos/com.azure.spring.data.cosmos.domain=com.fasterxml.jackson.databind,spring.core,spring.data.commons + + + + + + org.springframework + spring-core + 5.3.25 + + + commons-logging + commons-logging + + + + + org.springframework + spring-web + 5.3.25 + + + org.springframework + spring-beans + 5.3.25 + + + org.springframework + spring-context + 5.3.25 + + + org.springframework + spring-tx + 5.3.25 + + + org.springframework.data + spring-data-commons + 2.7.8 + + + org.slf4j + slf4j-api + + + + + org.springframework + spring-expression + 5.3.25 + + + com.azure + azure-cosmos + 4.44.0 + + + com.fasterxml.jackson.module + jackson-module-parameter-names + 2.13.5 + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + 2.13.5 + + + org.javatuples + javatuples + 1.2 + + + javax.annotation + javax.annotation-api + 1.3.2 + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + + org.mockito + mockito-core + 4.11.0 + test + + + junit + junit + 4.13.2 + test + + + org.springframework.boot + spring-boot-starter-test + 2.7.9 + test + + + com.vaadin.external.google + android-json + + + + + io.projectreactor + reactor-test + 3.4.27 + test + + + org.slf4j + slf4j-simple + 1.7.36 + test + + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + + org.slf4j + slf4j-api + 1.7.36 + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + org.springframework:spring-beans:[5.3.25] + org.springframework:spring-web:[5.3.25] + org.springframework:spring-tx:[5.3.25] + org.springframework:spring-expression:[5.3.25] + org.springframework:spring-core:[5.3.25] + org.springframework:spring-context:[5.3.25] + org.springframework.data:spring-data-commons:[2.7.8] + org.javatuples:javatuples:[1.2] + com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.13.5] + com.fasterxml.jackson.module:jackson-module-parameter-names:[2.13.5] + javax.annotation:javax.annotation-api:[1.3.2] + org.apache.commons:commons-lang3:[3.12.0] + org.slf4j:slf4j-api:[1.7.36] + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + + com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentProperty.java + + + + org.projectlombok + lombok + 1.18.6 + + + + + + + diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/Constants.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/Constants.java new file mode 100644 index 000000000000..99c751bf8bf0 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/Constants.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.cosmos.models.IndexingMode; + +/** + * Constants class of CosmosDB properties + */ +public final class Constants { + + /** + * Default container name. + */ + public static final String DEFAULT_CONTAINER_NAME = ""; + + /** + * Default indexing policy automatic. + */ + public static final boolean DEFAULT_INDEXING_POLICY_OVERWRITE_POLICY = false; + + /** + * Default indexing policy automatic. + */ + public static final boolean DEFAULT_INDEXING_POLICY_AUTOMATIC = true; + + /** + * Default indexing policy mode. + */ + public static final IndexingMode DEFAULT_INDEXING_POLICY_MODE = IndexingMode.CONSISTENT; + + /** + * Default repository implement postfix. + */ + public static final String DEFAULT_REPOSITORY_IMPLEMENT_POSTFIX = "Impl"; + + /** + * Default time to live. + */ + public static final int DEFAULT_TIME_TO_LIVE = -1; // Indicates never expire + + /** + * Default auto create container. + */ + public static final boolean DEFAULT_AUTO_CREATE_CONTAINER = true; + + /** + * Default auto scale. + */ + public static final boolean DEFAULT_AUTO_SCALE = false; + + /** + * ID property name. + */ + public static final String ID_PROPERTY_NAME = "id"; + + /** + * ETag property default name. + */ + public static final String ETAG_PROPERTY_DEFAULT_NAME = "_etag"; + + /** + * Cosmos module name. + */ + public static final String COSMOS_MODULE_NAME = "cosmos"; + + /** + * Cosmos module prefix. + */ + public static final String COSMOS_MODULE_PREFIX = "cosmos"; + + private static final String AZURE_SPRING_DATA_COSMOS = "az-sd-cos"; + + /** + * User agent suffix. + */ + public static final String USER_AGENT_SUFFIX = AZURE_SPRING_DATA_COSMOS + "/"; + + /** + * Object mapper bean name. + */ + public static final String OBJECT_MAPPER_BEAN_NAME = "cosmosObjectMapper"; + + /** + * Auditing handler bean name. + */ + public static final String AUDITING_HANDLER_BEAN_NAME = "cosmosAuditingHandler"; + + /** + * ISO-8601 compatible date pattern. + */ + public static final String ISO_8601_COMPATIBLE_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss:SSSXXX"; + + private Constants() { + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/CosmosFactory.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/CosmosFactory.java new file mode 100644 index 000000000000..ffaa77a8f19f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/CosmosFactory.java @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.common.PropertyLoader; +import java.lang.reflect.Field; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; + +/** + * Factory class for CosmosDb to create client + */ +public class CosmosFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(CosmosFactory.class); + + private final CosmosAsyncClient cosmosAsyncClient; + + /** + * Database Name to be used for operations. + * This is not practically tied to CosmosFactory instance. + * This can be updated even for same cosmosAsyncClient. + */ + protected String databaseName; + + private static final String USER_AGENT_SUFFIX = + Constants.USER_AGENT_SUFFIX + PropertyLoader.getProjectVersion(); + + private static String getUserAgentSuffix() { + return USER_AGENT_SUFFIX; + } + + /** + * Validate config and initialization + * + * @param cosmosAsyncClient cosmosAsyncClient + * @param databaseName databaseName + */ + public CosmosFactory(CosmosAsyncClient cosmosAsyncClient, String databaseName) { + Assert.notNull(cosmosAsyncClient, "cosmosAsyncClient must not be null!"); + Assert.notNull(databaseName, "databaseName must not be null!"); + + this.cosmosAsyncClient = cosmosAsyncClient; + this.databaseName = databaseName; + } + + /** + * To create a CosmosAsyncClient + * + * @return CosmosAsyncClient + */ + public CosmosAsyncClient getCosmosAsyncClient() { + return this.cosmosAsyncClient; + } + + /** + * Get Cosmos Database Name + * @return Cosmos Database Name + */ + public String getDatabaseName() { + return this.databaseName; + } + + /** + * This returns container name for each operation. + * By default, container name will be derived from entity definition. + * NOTE: Using this API will override container name defined on entity definition. + * + * @return container name + */ + public String overrideContainerName() { + return null; + } + + /** + * Create Cosmos Async Client + * + * @param cosmosClientBuilder CosmosClientBuilder + * @return CosmosAsyncClient + */ + public static CosmosAsyncClient createCosmosAsyncClient(CosmosClientBuilder cosmosClientBuilder) { + return updateCosmosClientBuilderWithUASuffix(cosmosClientBuilder).buildAsyncClient(); + } + + private static CosmosClientBuilder updateCosmosClientBuilderWithUASuffix(CosmosClientBuilder cosmosClientBuilder) { + cosmosClientBuilder.contentResponseOnWriteEnabled(true); + final String userAgentSuffixValue = getUserAgentSuffixValue(cosmosClientBuilder); + String userAgentSuffix = getUserAgentSuffix(); + if (!userAgentSuffixValue.contains(userAgentSuffix)) { + userAgentSuffix += userAgentSuffixValue; + } + + return cosmosClientBuilder.userAgentSuffix(userAgentSuffix); + } + + private static String getUserAgentSuffixValue(CosmosClientBuilder cosmosClientBuilder) { + try { + final Field userAgentSuffixField = cosmosClientBuilder.getClass().getDeclaredField("userAgentSuffix"); + userAgentSuffixField.setAccessible(true); + return (String) userAgentSuffixField.get(cosmosClientBuilder); + } catch (IllegalAccessException | NoSuchFieldException e) { + LOGGER.error("Error occurred while getting userAgentSuffix from CosmosClientBuilder", + e); + } + return ""; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/CosmosUtils.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/CosmosUtils.java new file mode 100644 index 000000000000..362f69b875e5 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/CosmosUtils.java @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.common; + +import com.azure.cosmos.CosmosDiagnostics; +import com.azure.cosmos.CosmosException; +import com.azure.cosmos.models.FeedResponse; +import com.azure.spring.data.cosmos.core.ResponseDiagnostics; +import com.azure.spring.data.cosmos.core.ResponseDiagnosticsProcessor; +import com.azure.spring.data.cosmos.exception.IllegalQueryException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; + +import java.util.UUID; + +/** + * Util class to fill and process response diagnostics + */ +public class CosmosUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(CosmosUtils.class); + + /** + * 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 cosmosDiagnostics response from cosmos + * @param feedResponse response from feed + */ + public static void fillAndProcessResponseDiagnostics( + ResponseDiagnosticsProcessor responseDiagnosticsProcessor, + CosmosDiagnostics cosmosDiagnostics, FeedResponse feedResponse) { + if (responseDiagnosticsProcessor == null) { + return; + } + ResponseDiagnostics.CosmosResponseStatistics cosmosResponseStatistics = null; + if (feedResponse != null) { + cosmosResponseStatistics = new ResponseDiagnostics.CosmosResponseStatistics(feedResponse); + } + if (cosmosDiagnostics == null && cosmosResponseStatistics == null) { + LOGGER.debug("Empty response diagnostics"); + return; + } + final ResponseDiagnostics responseDiagnostics = + new ResponseDiagnostics(cosmosDiagnostics, cosmosResponseStatistics); + + // Process response diagnostics + responseDiagnosticsProcessor.processResponseDiagnostics(responseDiagnostics); + } + + /** + * Generate ResponseDiagnostics with CosmosException diagnostics + * @param responseDiagnosticsProcessor response diagnostics processor + * @param cosmosException cosmos exception + */ + public static void fillAndProcessCosmosExceptionDiagnostics(ResponseDiagnosticsProcessor responseDiagnosticsProcessor, + CosmosException cosmosException) { + if (responseDiagnosticsProcessor == null) { + return; + } + if (cosmosException == null || cosmosException.getDiagnostics() == null) { + return; + } + fillAndProcessResponseDiagnostics(responseDiagnosticsProcessor, cosmosException.getDiagnostics(), null); + } + + /** + * ID value should be string value, real id type will be String, Integer, Long, + * all of these must be converted to String type. + * @param idValue id value to find + * @throws IllegalArgumentException thrown if id value fail the validation. + * @throws IllegalQueryException thrown if id value fail the validation. + * @return String id value + */ + public static String getStringIDValue(Object idValue) { + Assert.notNull(idValue, "id should not be null"); + if (idValue instanceof String) { + Assert.hasText(idValue.toString(), "id should not be empty or only whitespaces."); + return (String) idValue; + } else if (idValue instanceof Integer) { + return Integer.toString((Integer) idValue); + } else if (idValue instanceof Long) { + return Long.toString((Long) idValue); + } else if (idValue instanceof UUID) { + return idValue.toString(); + } else { + throw new IllegalQueryException("Type of id field must be String or Integer or Long or UUID"); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/ExpressionResolver.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/ExpressionResolver.java new file mode 100644 index 000000000000..2d4261016a72 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/ExpressionResolver.java @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/MacAddress.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/MacAddress.java new file mode 100644 index 000000000000..41ff8997e186 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/Memoizer.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/Memoizer.java new file mode 100644 index 000000000000..f628528e0e04 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/Memoizer.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/PropertyLoader.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/PropertyLoader.java new file mode 100644 index 000000000000..6abfa2571cc4 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/PropertyLoader.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/azure-spring-data-cosmos.properties"; + + private static final String APPLICATION_PROPERTY_FILE = "/application.properties"; + + private static final String APPLICATION_YML_FILE = "/application.yml"; + + private PropertyLoader() { + } + + /** + * Get project version from /META-INF/azure-spring-data-cosmos.properties + * + * @return String project version + */ + public static String getProjectVersion() { + return getPropertyByName("project.version", PROJECT_PROPERTY_FILE); + } + + 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/package-info.java new file mode 100644 index 000000000000..0ddbe6ad976a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/common/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Azure Event Hubs service abstraction. + */ +package com.azure.spring.cloud.service.eventhubs; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/AbstractCosmosConfiguration.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/AbstractCosmosConfiguration.java new file mode 100644 index 000000000000..eedc74669393 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/AbstractCosmosConfiguration.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.config; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.fasterxml.jackson.databind.ObjectMapper; +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; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; + +/** + * To configure cosmos with client, cosmos factory and template + */ +@Configuration +public abstract class AbstractCosmosConfiguration extends CosmosConfigurationSupport { + + /** + * Declare CosmosFactory bean. + * + * @param cosmosAsyncClient of cosmosFactory + * @return CosmosFactory bean + */ + @Bean + public CosmosFactory cosmosFactory(CosmosAsyncClient cosmosAsyncClient) { + return new CosmosFactory(cosmosAsyncClient, getDatabaseName()); + } + + /** + * Declare MappingCosmosConverter bean. + * + * @param cosmosMappingContext cosmosMappingContext + * @return MappingCosmosConverter bean + */ + @Bean + public MappingCosmosConverter mappingCosmosConverter(CosmosMappingContext cosmosMappingContext) { + return new MappingCosmosConverter(cosmosMappingContext, objectMapper); + } + + /** + * Declare CosmosAsyncClient bean. + * + * @param cosmosClientBuilder cosmosClientBuilder + * @return CosmosAsyncClient bean + */ + @Bean + public CosmosAsyncClient cosmosAsyncClient(CosmosClientBuilder cosmosClientBuilder) { + return CosmosFactory.createCosmosAsyncClient(cosmosClientBuilder); + } + + @Qualifier(Constants.OBJECT_MAPPER_BEAN_NAME) + @Autowired(required = false) + private ObjectMapper objectMapper; + + @Qualifier(Constants.AUDITING_HANDLER_BEAN_NAME) + @Autowired(required = false) + private IsNewAwareAuditingHandler cosmosAuditingHandler; + + /** + * Declare CosmosTemplate bean. + * + * @param cosmosFactory cosmosFactory + * @param cosmosConfig cosmosConfig + * @param mappingCosmosConverter mappingCosmosConverter + * @return CosmosTemplate bean + */ + @Bean + public CosmosTemplate cosmosTemplate(CosmosFactory cosmosFactory, + CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter) { + return new CosmosTemplate(cosmosFactory, cosmosConfig, mappingCosmosConverter, cosmosAuditingHandler); + } + + /** + * Declare ReactiveCosmosTemplate bean. + * + * @param cosmosFactory cosmosFactory + * @param cosmosConfig cosmosConfig + * @param mappingCosmosConverter mappingCosmosConverter + * @return ReactiveCosmosTemplate bean + */ + @Bean + public ReactiveCosmosTemplate reactiveCosmosTemplate(CosmosFactory cosmosFactory, + CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter) { + return new ReactiveCosmosTemplate(cosmosFactory, cosmosConfig, mappingCosmosConverter, cosmosAuditingHandler); + } + + /** + * Declare CosmosConfig bean + * + * @return CosmosConfig bean + */ + @Bean + public CosmosConfig cosmosConfig() { + return new CosmosConfig.CosmosConfigBuilder().build(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/CosmosConfig.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/CosmosConfig.java new file mode 100644 index 000000000000..e46e987c9035 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/CosmosConfig.java @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.config; + +import com.azure.spring.data.cosmos.core.ResponseDiagnosticsProcessor; + +import java.beans.ConstructorProperties; + +/** + * Config properties of CosmosDB + */ +public class CosmosConfig { + + private final ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + + private final DatabaseThroughputConfig databaseThroughputConfig; + + private final boolean queryMetricsEnabled; + + private final int maxDegreeOfParallelism; + + private final int maxBufferedItemCount; + + private final int responseContinuationTokenLimitInKb; + + /** + * Initialization + * + * @param responseDiagnosticsProcessor must not be {@literal null} + * @param queryMetricsEnabled must not be {@literal null} + */ + @ConstructorProperties({"responseDiagnosticsProcessor", "queryMetricsEnabled"}) + public CosmosConfig(ResponseDiagnosticsProcessor responseDiagnosticsProcessor, + boolean queryMetricsEnabled) { + this(responseDiagnosticsProcessor, null, queryMetricsEnabled); + } + + /** + * Initialization + * + * @param responseDiagnosticsProcessor must not be {@literal null} + * @param databaseThroughputConfig may be {@literal null} + * @param queryMetricsEnabled must not be {@literal null} + */ + @ConstructorProperties({"responseDiagnosticsProcessor", "databaseThroughputConfig", "queryMetricsEnabled"}) + public CosmosConfig(ResponseDiagnosticsProcessor responseDiagnosticsProcessor, + DatabaseThroughputConfig databaseThroughputConfig, + boolean queryMetricsEnabled) { + this.responseDiagnosticsProcessor = responseDiagnosticsProcessor; + this.databaseThroughputConfig = databaseThroughputConfig; + this.queryMetricsEnabled = queryMetricsEnabled; + this.maxDegreeOfParallelism = 0; + this.maxBufferedItemCount = 0; + this.responseContinuationTokenLimitInKb = 0; + } + + /** + * Initialization + * + * @param responseDiagnosticsProcessor must not be {@literal null} + * @param databaseThroughputConfig may be {@literal null} + * @param queryMetricsEnabled must not be {@literal null} + * @param maxDegreeOfParallelism must not be {@literal null} + */ + @ConstructorProperties({"responseDiagnosticsProcessor", "databaseThroughputConfig", "queryMetricsEnabled", "maxDegreeOfParallelism"}) + CosmosConfig(ResponseDiagnosticsProcessor responseDiagnosticsProcessor, + DatabaseThroughputConfig databaseThroughputConfig, + boolean queryMetricsEnabled, + int maxDegreeOfParallelism) { + this.responseDiagnosticsProcessor = responseDiagnosticsProcessor; + this.databaseThroughputConfig = databaseThroughputConfig; + this.queryMetricsEnabled = queryMetricsEnabled; + this.maxDegreeOfParallelism = maxDegreeOfParallelism; + this.maxBufferedItemCount = 0; + this.responseContinuationTokenLimitInKb = 0; + } + + /** + * Initialization + * + * @param responseDiagnosticsProcessor must not be {@literal null} + * @param databaseThroughputConfig may be {@literal null} + * @param queryMetricsEnabled must not be {@literal null} + * @param maxDegreeOfParallelism must not be {@literal null} + * @param maxBufferedItemCount must not be {@literal null} + */ + @ConstructorProperties({"responseDiagnosticsProcessor", "databaseThroughputConfig", "queryMetricsEnabled", "maxDegreeOfParallelism", "maxBufferedItemCount"}) + CosmosConfig(ResponseDiagnosticsProcessor responseDiagnosticsProcessor, + DatabaseThroughputConfig databaseThroughputConfig, + boolean queryMetricsEnabled, + int maxDegreeOfParallelism, + int maxBufferedItemCount) { + this.responseDiagnosticsProcessor = responseDiagnosticsProcessor; + this.databaseThroughputConfig = databaseThroughputConfig; + this.queryMetricsEnabled = queryMetricsEnabled; + this.maxDegreeOfParallelism = maxDegreeOfParallelism; + this.maxBufferedItemCount = maxBufferedItemCount; + this.responseContinuationTokenLimitInKb = 0; + } + + /** + * Initialization + * + * @param responseDiagnosticsProcessor must not be {@literal null} + * @param databaseThroughputConfig may be {@literal null} + * @param queryMetricsEnabled must not be {@literal null} + * @param maxDegreeOfParallelism must not be {@literal null} + * @param maxBufferedItemCount must not be {@literal null} + * @param responseContinuationTokenLimitInKb must not be {@literal null} + */ + @ConstructorProperties({"responseDiagnosticsProcessor", "databaseThroughputConfig", "queryMetricsEnabled", "maxDegreeOfParallelism", "maxBufferedItemCount", "responseContinuationTokenLimitInKb"}) + CosmosConfig(ResponseDiagnosticsProcessor responseDiagnosticsProcessor, + DatabaseThroughputConfig databaseThroughputConfig, + boolean queryMetricsEnabled, + int maxDegreeOfParallelism, + int maxBufferedItemCount, + int responseContinuationTokenLimitInKb) { + this.responseDiagnosticsProcessor = responseDiagnosticsProcessor; + this.databaseThroughputConfig = databaseThroughputConfig; + this.queryMetricsEnabled = queryMetricsEnabled; + this.maxDegreeOfParallelism = maxDegreeOfParallelism; + this.maxBufferedItemCount = maxBufferedItemCount; + this.responseContinuationTokenLimitInKb = responseContinuationTokenLimitInKb; + } + + /** + * Gets response diagnostics processor + * + * @return ResponseDiagnosticsProcessor + */ + public ResponseDiagnosticsProcessor getResponseDiagnosticsProcessor() { + return responseDiagnosticsProcessor; + } + + /** + * Gets the option to enable query metrics + * + * @return boolean, whether to enable query metrics + */ + public boolean isQueryMetricsEnabled() { + return queryMetricsEnabled; + } + + /** + * Gets the value of maxDegreeOfParallelism + * + * @return int, value of maxDegreeOfParallelism + */ + public int getMaxDegreeOfParallelism() { + return maxDegreeOfParallelism; + } + + /** + * Gets the value of maxBufferedItemCount + * + * @return int, value of maxBufferedItemCount + */ + public int getMaxBufferedItemCount() { + return maxBufferedItemCount; + } + + /** + * Gets the value of responseContinuationTokenLimitInKb + * + * @return int, value of responseContinuationTokenLimitInKb + */ + public int getResponseContinuationTokenLimitInKb() { + return responseContinuationTokenLimitInKb; + } + + /** + * Gets the database throughput configuration. + * + * @return DatabaseThroughputConfig, or null if no database throughput is configured + */ + public DatabaseThroughputConfig getDatabaseThroughputConfig() { + return databaseThroughputConfig; + } + + /** + * Create a CosmosConfigBuilder instance + * + * @return CosmosConfigBuilder + */ + public static CosmosConfigBuilder builder() { + return new CosmosConfigBuilder(); + } + + /** + * Builder class for cosmos config + */ + public static class CosmosConfigBuilder { + private ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + private DatabaseThroughputConfig databaseThroughputConfig; + private boolean queryMetricsEnabled; + private int maxDegreeOfParallelism; + private int maxBufferedItemCount; + private int responseContinuationTokenLimitInKb; + CosmosConfigBuilder() { + } + + + /** + * Set responseDiagnosticsProcessor + * + * @param responseDiagnosticsProcessor value to initialize + * @return CosmosConfigBuilder + */ + public CosmosConfigBuilder responseDiagnosticsProcessor(ResponseDiagnosticsProcessor + responseDiagnosticsProcessor) { + this.responseDiagnosticsProcessor = responseDiagnosticsProcessor; + return this; + } + + /** + * Set queryMetricsEnabled + * + * @param queryMetricsEnabled value to initialize + * @return CosmosConfigBuilder + */ + public CosmosConfigBuilder enableQueryMetrics(boolean queryMetricsEnabled) { + this.queryMetricsEnabled = queryMetricsEnabled; + return this; + } + + /** + * Set maxDegreeOfParallelism + * + * @param maxDegreeOfParallelism value to initialize + * @return CosmosConfigBuilder + */ + public CosmosConfigBuilder maxDegreeOfParallelism(int maxDegreeOfParallelism) { + this.maxDegreeOfParallelism = maxDegreeOfParallelism; + return this; + } + + /** + * Set maxBufferedItemCount + * + * @param maxBufferedItemCount value to initialize + * @return CosmosConfigBuilder + */ + public CosmosConfigBuilder maxBufferedItemCount(int maxBufferedItemCount) { + this.maxBufferedItemCount = maxBufferedItemCount; + return this; + } + + /** + * Set responseContinuationTokenLimitInKb + * + * @param responseContinuationTokenLimitInKb value to initialize + * @return CosmosConfigBuilder + */ + public CosmosConfigBuilder responseContinuationTokenLimitInKb(int responseContinuationTokenLimitInKb) { + this.responseContinuationTokenLimitInKb = responseContinuationTokenLimitInKb; + return this; + } + + /** + * Enable database throughput + * + * @param autoscale Autoscaling + * @param requestUnits Request units + * @return CosmosConfigBuilder + */ + public CosmosConfigBuilder enableDatabaseThroughput(boolean autoscale, int requestUnits) { + this.databaseThroughputConfig = new DatabaseThroughputConfig(autoscale, requestUnits); + return this; + } + + /** + * Build a CosmosConfig instance + * + * @return CosmosConfig + */ + public CosmosConfig build() { + return new CosmosConfig(this.responseDiagnosticsProcessor, this.databaseThroughputConfig, this.queryMetricsEnabled, + this.maxDegreeOfParallelism, this.maxBufferedItemCount, this.responseContinuationTokenLimitInKb); + } + + @Override + public String toString() { + return "CosmosConfigBuilder{" + + "responseDiagnosticsProcessor=" + responseDiagnosticsProcessor + + ", databaseThroughputConfig=" + databaseThroughputConfig + + ", queryMetricsEnabled=" + queryMetricsEnabled + + ", maxDegreeOfParallelism=" + maxDegreeOfParallelism + + ", maxBufferedItemCount=" + maxBufferedItemCount + + ", responseContinuationTokenLimitInKb=" + responseContinuationTokenLimitInKb + + '}'; + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/CosmosConfigurationSupport.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/CosmosConfigurationSupport.java new file mode 100644 index 000000000000..60c48d3e392e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/CosmosConfigurationSupport.java @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.config; + +import com.azure.spring.data.cosmos.common.ExpressionResolver; +import com.azure.spring.data.cosmos.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 { + + /** + * Return the name of the database to connect to + * + * @return must not be {@literal null}. + */ + protected abstract String getDatabaseName(); + + /** + * 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; + } + + /** + * Get the mapping base package name. + * + * @return Base mapping package name set. + */ + 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/DatabaseThroughputConfig.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/DatabaseThroughputConfig.java new file mode 100644 index 000000000000..53d300562394 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/DatabaseThroughputConfig.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.config; + +/** + * Throughput config for database creation + */ +public class DatabaseThroughputConfig { + + private final boolean autoScale; + private final int requestUnits; + + /** + * Constructor. + * + * @param autoScale flag to enable autoscale. + * @param requestUnits the request units. + */ + public DatabaseThroughputConfig(boolean autoScale, int requestUnits) { + this.autoScale = autoScale; + this.requestUnits = requestUnits; + } + + /** + * Flag to indicate whether autoscale is enabled. + * + * @return Flag to indicate whether autoscale is enabled. + */ + public boolean isAutoScale() { + return autoScale; + } + + /** + * Get the request units. + * + * @return The request units. + */ + public int getRequestUnits() { + return requestUnits; + } + + @Override + public String toString() { + return "DatabaseThroughputConfig{" + + "autoScale=" + autoScale + + ", requestUnits=" + requestUnits + + '}'; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/config/package-info.java new file mode 100644 index 000000000000..af2cccf865d6 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.config; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java new file mode 100644 index 000000000000..e2f5909a7882 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java @@ -0,0 +1,390 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; + +/** + * Interface for cosmosDB operations + */ +public interface CosmosOperations { + + /** + * To get container name by domainType + * + * @param domainType class type + * @return String + */ + String getContainerName(Class domainType); + + /** + * Creates container if not exists + * + * @param information CosmosEntityInformation + * @return CosmosContainerProperties + */ + CosmosContainerProperties createContainerIfNotExists(CosmosEntityInformation information); + + /** + * Get properties for specified container + * + * @param containerName String + * @return CosmosContainerProperties + */ + CosmosContainerProperties getContainerProperties(String containerName); + + /** + * Replace container properties for the specified container + * + * @param containerName String + * @param properties CosmosContainerProperties + * @return CosmosContainerProperties + */ + CosmosContainerProperties replaceContainerProperties(String containerName, CosmosContainerProperties properties); + + /** + * Find the DocumentQuery, find all the items specified by domain type. + * + * @param domainType the domain type + * @param class type of domain + * @return results in an Iterable + */ + Iterable 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 results in an Iterable + */ + Iterable 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 results in an Iterable + */ + Iterable 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); + + /** + * patches item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param type class of domain type + * @return the patched item + */ + T patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations); + + /** + * patches item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched item + */ + T patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options); + /** + * 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); + + /** + * Inserts item + * @param containerName must not be {@literal null} + * @param objectToSave must not be {@literal null} + * @param type class of domain type + * @return the inserted item + */ + T insert(String containerName, T objectToSave); + + /** + * Upserts an item with partition key + * + * @param object upsert object + * @param type of upsert object + */ + void upsert(T object); + + /** + * Upserts an item into container with partition key + * + * @param containerName the container name + * @param object upsert object + * @param type of upsert object + */ + void upsert(String containerName, T object); + + /** + * Upserts an item and return item properties + * + * @param containerName the container name + * @param object upsert object + * @param type of upsert object + * @return upsert object entity + */ + T upsertAndReturnEntity(String containerName, T object); + + /** + * 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 using entity + * + * @param type class of domain type + * @param containerName the container name + * @param entity the entity object + */ + void deleteEntity(String containerName, T entity); + + /** + * Delete all items in a container + * + * @param containerName the container name + * @param domainType the domainType + */ + void deleteAll(String containerName, Class domainType); + + /** + * 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 Iterable + */ + Iterable delete(CosmosQuery 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 results in an Iterable + */ + Iterable find(CosmosQuery 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 results in an Iterable + */ + Iterable 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(CosmosQuery 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 results as 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 results as Page + */ + Page paginationQuery(CosmosQuery query, Class domainType, String containerName); + + /** + * Slice query + * + * @param query the document query + * @param domainType type class + * @param containerName the container name + * @param type class of domainType + * @return results as Slice + */ + Slice sliceQuery(CosmosQuery query, Class domainType, String containerName); + + /** + * Run custom SQL query + * + * @param querySpec the query spec + * @param pageable the pageable + * @param domainType the domain type + * @param returnType the return type + * @param the type parameter + * @return the Page + */ + Slice runSliceQuery(SqlQuerySpec querySpec, Pageable pageable, Class domainType, Class returnType); + + /** + * Count + * + * @param containerName the container name + * @return count result + */ + long count(String containerName); + + /** + * Count + * + * @param query the document query + * @param containerName the container name + * @param type class of domainType + * @return count result + */ + long count(CosmosQuery query, String containerName); + + /** + * Count + * + * @param querySpec the document query spec + * @param containerName the container name + * @param type class of domainType + * @return count result + */ + long count(SqlQuerySpec querySpec, String containerName); + + /** + * To get converter + * + * @return MappingCosmosConverter + */ + MappingCosmosConverter getConverter(); + + /** + * Run the query. + * + * @param the type parameter + * @param querySpec the query spec + * @param domainType the domain type + * @param returnType the return type + * @return the Iterable + */ + Iterable runQuery(SqlQuerySpec querySpec, Class domainType, Class returnType); + + /** + * Run the query. + * + * @param the type parameter + * @param querySpec the query spec + * @param sort the sort order + * @param domainType the domain type + * @param returnType the return type + * @return the Iterable + */ + Iterable runQuery(SqlQuerySpec querySpec, Sort sort, Class domainType, Class returnType); + + /** + * Run the query. + * + * @param the type parameter + * @param querySpec the query spec + * @param pageable the pageable + * @param domainType the domain type + * @param returnType the return type + * @return the Page + */ + Page runPaginationQuery(SqlQuerySpec querySpec, Pageable pageable, Class domainType, Class returnType); + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java new file mode 100644 index 000000000000..3cd50478de69 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java @@ -0,0 +1,1132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosAsyncContainer; +import com.azure.cosmos.CosmosAsyncDatabase; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosContainerResponse; +import com.azure.cosmos.models.CosmosDatabaseResponse; +import com.azure.cosmos.models.CosmosItemRequestOptions; +import com.azure.cosmos.models.CosmosItemResponse; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.CosmosQueryRequestOptions; +import com.azure.cosmos.models.FeedResponse; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.cosmos.models.ThroughputProperties; +import com.azure.cosmos.models.UniqueKeyPolicy; +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.common.CosmosUtils; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.config.DatabaseThroughputConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.generator.CountQueryGenerator; +import com.azure.spring.data.cosmos.core.generator.FindQuerySpecGenerator; +import com.azure.spring.data.cosmos.core.generator.NativeQueryGenerator; +import com.azure.spring.data.cosmos.core.mapping.event.AfterLoadEvent; +import com.azure.spring.data.cosmos.core.mapping.event.CosmosMappingEvent; +import com.azure.spring.data.cosmos.core.query.CosmosPageImpl; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.CosmosSliceImpl; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.exception.CosmosExceptionUtils; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.databind.JsonNode; +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.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * Template class for cosmos db + */ +public class CosmosTemplate implements CosmosOperations, ApplicationContextAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(CosmosTemplate.class); + + private final MappingCosmosConverter mappingCosmosConverter; + private final IsNewAwareAuditingHandler cosmosAuditingHandler; + + private final CosmosFactory cosmosFactory; + private final ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + private final boolean queryMetricsEnabled; + private final int maxDegreeOfParallelism; + private final int maxBufferedItemCount; + private final int responseContinuationTokenLimitInKb; + private final DatabaseThroughputConfig databaseThroughputConfig; + + private ApplicationContext applicationContext; + + /** + * Initialization + * + * @param client must not be {@literal null} + * @param databaseName must not be {@literal null} + * @param cosmosConfig must not be {@literal null} + * @param mappingCosmosConverter must not be {@literal null} + * @param cosmosAuditingHandler can be {@literal null} + */ + public CosmosTemplate(CosmosAsyncClient client, String databaseName, + CosmosConfig cosmosConfig, MappingCosmosConverter mappingCosmosConverter, + IsNewAwareAuditingHandler cosmosAuditingHandler) { + this(new CosmosFactory(client, databaseName), cosmosConfig, mappingCosmosConverter, cosmosAuditingHandler); + } + + /** + * Initialization + * + * @param client must not be {@literal null} + * @param databaseName must not be {@literal null} + * @param cosmosConfig must not be {@literal null} + * @param mappingCosmosConverter must not be {@literal null} + */ + public CosmosTemplate(CosmosAsyncClient client, String databaseName, + CosmosConfig cosmosConfig, MappingCosmosConverter mappingCosmosConverter) { + this(new CosmosFactory(client, databaseName), cosmosConfig, mappingCosmosConverter, null); + } + + /** + * Initialization + * + * @param cosmosFactory must not be {@literal null} + * @param cosmosConfig must not be {@literal null} + * @param mappingCosmosConverter must not be {@literal null} + * @param cosmosAuditingHandler can be {@literal null} + */ + public CosmosTemplate(CosmosFactory cosmosFactory, + CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter, + IsNewAwareAuditingHandler cosmosAuditingHandler) { + Assert.notNull(cosmosFactory, "CosmosFactory must not be null!"); + Assert.notNull(mappingCosmosConverter, "MappingCosmosConverter must not be null!"); + this.mappingCosmosConverter = mappingCosmosConverter; + this.cosmosAuditingHandler = cosmosAuditingHandler; + this.cosmosFactory = cosmosFactory; + this.responseDiagnosticsProcessor = cosmosConfig.getResponseDiagnosticsProcessor(); + this.queryMetricsEnabled = cosmosConfig.isQueryMetricsEnabled(); + this.maxDegreeOfParallelism = cosmosConfig.getMaxDegreeOfParallelism(); + this.maxBufferedItemCount = cosmosConfig.getMaxBufferedItemCount(); + this.responseContinuationTokenLimitInKb = cosmosConfig.getResponseContinuationTokenLimitInKb(); + this.databaseThroughputConfig = cosmosConfig.getDatabaseThroughputConfig(); + } + + /** + * Initialization + * + * @param cosmosFactory must not be {@literal null} + * @param cosmosConfig must not be {@literal null} + * @param mappingCosmosConverter must not be {@literal null} + */ + public CosmosTemplate(CosmosFactory cosmosFactory, + CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter) { + this(cosmosFactory, cosmosConfig, mappingCosmosConverter, null); + } + + private String getDatabaseName() { + return this.cosmosFactory.getDatabaseName(); + } + + private CosmosAsyncClient getCosmosAsyncClient() { + return this.cosmosFactory.getCosmosAsyncClient(); + } + + /** + * Sets the application context + * + * @param applicationContext must not be {@literal null} + * @throws BeansException the bean exception + */ + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + /** + * 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) { + 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 type class of domain type + * @return the inserted item + */ + @Override + public T insert(String containerName, T objectToSave) { + return insert(containerName, objectToSave, null); + } + + /** + * 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"); + + @SuppressWarnings("unchecked") final Class domainType = (Class) objectToSave.getClass(); + containerName = getContainerName(domainType); + + markAuditedIfConfigured(objectToSave); + generateIdIfNullAndAutoGenerationEnabled(objectToSave, domainType); + + final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(objectToSave); + + LOGGER.debug("execute createItem in database {} container {}", this.getDatabaseName(), + containerName); + + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + + // if the partition key is null, SDK will get the partitionKey from the object + final CosmosItemResponse response = this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .createItem(originalItem, partitionKey, options) + .publishOn(Schedulers.parallel()) + .doOnNext(cosmosItemResponse -> + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to insert item", throwable, + this.responseDiagnosticsProcessor)) + .block(); + + assert response != null; + return toDomainObject(domainType, response.getItem()); + } + + /** + * Patches item + * + * applies partial update (patch) to an item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param patchOperations must not be {@literal null} + * @param type class of domain type + * @return the patched item + */ + @Override + public T patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations) { + return patch(id, partitionKey, domainType, patchOperations, null); + } + + /** + * applies partial update (patch) to an item with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null} + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched item + */ + public T patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options) { + Assert.notNull(patchOperations, "expected non-null cosmosPatchOperations"); + + final String containerName = getContainerName(domainType); + Assert.notNull(id, "id should not be null"); + Assert.notNull(partitionKey, "partitionKey should not be null, empty or only whitespaces"); + Assert.notNull(patchOperations, "patchOperations should not be null, empty or only whitespaces"); + + LOGGER.debug("execute patchItem in database {} container {}", this.getDatabaseName(), + containerName); + + if (options == null) { + options = new CosmosPatchItemRequestOptions(); + } + final CosmosItemResponse response = this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .patchItem(id.toString(), partitionKey, patchOperations, options, JsonNode.class) + .publishOn(Schedulers.parallel()) + .doOnNext(cosmosItemResponse -> + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to patch item", throwable, + this.responseDiagnosticsProcessor)) + .block(); + assert response != null; + return toDomainObject(domainType, response.getItem()); + } + + @SuppressWarnings("unchecked") + private void generateIdIfNullAndAutoGenerationEnabled(T originalItem, Class type) { + CosmosEntityInformation entityInfo = CosmosEntityInformation.getInstance(type); + if (entityInfo.shouldGenerateId() && ReflectionUtils.getField(entityInfo.getIdField(), originalItem) == null) { + ReflectionUtils.setField(entityInfo.getIdField(), originalItem, UUID.randomUUID().toString()); + } + } + + /** + * 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"); + String idToQuery = CosmosUtils.getStringIDValue(id); + final String containerName = getContainerName(domainType); + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .readItem(idToQuery, partitionKey, JsonNode.class) + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null); + return Mono.justOrEmpty(emitOnLoadEventAndConvertToDomainObject(domainType, containerName, cosmosItemResponse.getItem())); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.findAPIExceptionHandler("Failed to find item", throwable, + this.responseDiagnosticsProcessor)) + .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"); + String finalContainerName = getContainerNameOverride(containerName); + final String query = "select * from root where root.id = @ROOT_ID"; + final SqlParameter param = new SqlParameter("@ROOT_ID", CosmosUtils.getStringIDValue(id)); + final SqlQuerySpec sqlQuerySpec = new SqlQuerySpec(query, param); + final CosmosQueryRequestOptions options = new CosmosQueryRequestOptions(); + options.setQueryMetricsEnabled(this.queryMetricsEnabled); + options.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + options.setMaxBufferedItemCount(this.maxBufferedItemCount); + options.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(finalContainerName) + .queryItems(sqlQuerySpec, options, JsonNode.class) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), cosmosItemFeedResponse); + return Mono.justOrEmpty(cosmosItemFeedResponse + .getResults() + .stream() + .map(cosmosItem -> emitOnLoadEventAndConvertToDomainObject(domainType, finalContainerName, cosmosItem)) + .findFirst()); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.findAPIExceptionHandler("Failed to find item", throwable, + this.responseDiagnosticsProcessor)) + .blockFirst(); + } + + /** + * Upserts an item with partition key + * + * @param object upsert object + * @param type of upsert object + */ + public void upsert(T object) { + Assert.notNull(object, "Upsert object should not be null"); + + upsert(getContainerName(object.getClass()), object); + } + + /** + * Upserts an item into container with partition key + * + * @param containerName the container name + * @param object upsert object + * @param type of upsert object + */ + public void upsert(String containerName, T object) { + upsertAndReturnEntity(containerName, object); + } + + /** + * Upserts an item and return item properties + * + * @param containerName the container name + * @param object upsert object + * @param type of upsert object + * @return upsert object entity + */ + public T upsertAndReturnEntity(String containerName, T object) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + Assert.notNull(object, "Upsert object should not be null"); + containerName = getContainerName(object.getClass()); + markAuditedIfConfigured(object); + + final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(object); + + LOGGER.debug("execute upsert item in database {} container {}", this.getDatabaseName(), + containerName); + + @SuppressWarnings("unchecked") final Class domainType = (Class) object.getClass(); + containerName = getContainerName(domainType); + + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + applyVersioning(domainType, originalItem, options); + + final CosmosItemResponse cosmosItemResponse = this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .upsertItem(originalItem, options) + .publishOn(Schedulers.parallel()) + .doOnNext(response -> CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + response.getDiagnostics(), null)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to upsert item", throwable, + this.responseDiagnosticsProcessor)) + .block(); + + assert cosmosItemResponse != null; + return toDomainObject(domainType, cosmosItemResponse.getItem()); + } + + /** + * 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 Iterable 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 Iterable 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 CosmosQuery query = new CosmosQuery(Criteria.getInstance(CriteriaType.ALL)); + + return findItems(query, containerName, domainType); + } + + @Override + public Iterable 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 CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + cosmosQueryRequestOptions.setPartitionKey(partitionKey); + cosmosQueryRequestOptions.setQueryMetricsEnabled(this.queryMetricsEnabled); + cosmosQueryRequestOptions.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + cosmosQueryRequestOptions.setMaxBufferedItemCount(this.maxBufferedItemCount); + cosmosQueryRequestOptions.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .queryItems("SELECT * FROM r", cosmosQueryRequestOptions, JsonNode.class) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.getResults()); + }) + .map(jsonNode -> emitOnLoadEventAndConvertToDomainObject(domainType, containerName, jsonNode)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to find items", throwable, + this.responseDiagnosticsProcessor)) + .toIterable(); + } + + /** + * 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"); + containerName = getContainerName(domainType); + final CosmosQuery query = new CosmosQuery(Criteria.getInstance(CriteriaType.ALL)); + + this.delete(query, domainType, containerName); + } + + @Override + public void deleteContainer(@NonNull String containerName) { + Assert.hasText(containerName, "containerName should have text."); + this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .delete() + .publishOn(Schedulers.parallel()) + .doOnNext(response -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + response.getDiagnostics(), null); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to delete container", throwable, + this.responseDiagnosticsProcessor)) + .block(); + } + + @Override + public String getContainerName(Class domainType) { + Assert.notNull(domainType, "domainType should not be null"); + return getContainerNameOverride(CosmosEntityInformation.getInstance(domainType).getContainerName()); + } + /** + * Check if an overridden version of containerName is present, and if so, return it + * + * @param containerName Container name of database + * @return containerName + */ + public String getContainerNameOverride(String containerName) { + if (this.cosmosFactory.overrideContainerName() != null) { + return this.cosmosFactory.overrideContainerName(); + } + Assert.notNull(containerName, "containerName should not be null"); + return containerName; + } + + @Override + public CosmosContainerProperties createContainerIfNotExists(CosmosEntityInformation information) { + + final CosmosContainerResponse response = createDatabaseIfNotExists() + .publishOn(Schedulers.parallel()) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to create database", throwable, + this.responseDiagnosticsProcessor)) + .flatMap(cosmosDatabaseResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosDatabaseResponse.getDiagnostics(), null); + + final CosmosContainerProperties cosmosContainerProperties = + new CosmosContainerProperties(getContainerNameOverride(information.getContainerName()), information.getPartitionKeyPath()); + cosmosContainerProperties.setDefaultTimeToLiveInSeconds(information.getTimeToLive()); + cosmosContainerProperties.setIndexingPolicy(information.getIndexingPolicy()); + final UniqueKeyPolicy uniqueKeyPolicy = information.getUniqueKeyPolicy(); + if (uniqueKeyPolicy != null) { + cosmosContainerProperties.setUniqueKeyPolicy(uniqueKeyPolicy); + } + + CosmosAsyncDatabase cosmosAsyncDatabase = this.getCosmosAsyncClient() + .getDatabase(cosmosDatabaseResponse.getProperties().getId()); + Mono cosmosContainerResponseMono; + + if (information.getRequestUnit() == null) { + cosmosContainerResponseMono = + cosmosAsyncDatabase.createContainerIfNotExists(cosmosContainerProperties); + } else { + ThroughputProperties throughputProperties = information.isAutoScale() + ? ThroughputProperties.createAutoscaledThroughput(information.getRequestUnit()) + : ThroughputProperties.createManualThroughput(information.getRequestUnit()); + cosmosContainerResponseMono = + cosmosAsyncDatabase.createContainerIfNotExists(cosmosContainerProperties, + throughputProperties); + } + + return cosmosContainerResponseMono + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to create container", + throwable, this.responseDiagnosticsProcessor)) + .doOnNext(cosmosContainerResponse -> + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosContainerResponse.getDiagnostics(), null)); + }) + .block(); + assert response != null; + return response.getProperties(); + } + + private Mono createDatabaseIfNotExists() { + if (databaseThroughputConfig == null) { + return this.getCosmosAsyncClient() + .createDatabaseIfNotExists(this.getDatabaseName()); + } else { + ThroughputProperties throughputProperties = databaseThroughputConfig.isAutoScale() + ? ThroughputProperties.createAutoscaledThroughput(databaseThroughputConfig.getRequestUnits()) + : ThroughputProperties.createManualThroughput(databaseThroughputConfig.getRequestUnits()); + return this.getCosmosAsyncClient() + .createDatabaseIfNotExists(this.getDatabaseName(), throughputProperties); + } + } + + @Override + public CosmosContainerProperties getContainerProperties(String containerName) { + containerName = getContainerNameOverride(containerName); + final CosmosContainerResponse response = this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .read() + .block(); + assert response != null; + return response.getProperties(); + } + + @Override + public CosmosContainerProperties replaceContainerProperties(String containerName, + CosmosContainerProperties properties) { + CosmosContainerResponse response = this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .replace(properties) + .block(); + assert response != null; + return response.getProperties(); + } + + /** + * Deletes the item by id and partition key. + * + * @param containerName Container name of database + * @param id item id + * @param partitionKey the partition key + */ + public void deleteById(String containerName, Object id, PartitionKey partitionKey) { + deleteById(containerName, id, partitionKey, new CosmosItemRequestOptions()); + } + + /** + * Deletes the entity + * + * @param type class of domain type + * @param containerName the container name + * @param entity the entity object + */ + @Override + public void deleteEntity(String containerName, T entity) { + containerName = getContainerNameOverride(containerName); + Assert.notNull(entity, "entity to be deleted should not be null"); + @SuppressWarnings("unchecked") final Class domainType = (Class) entity.getClass(); + final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(entity); + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + applyVersioning(entity.getClass(), originalItem, options); + deleteItem(originalItem, containerName, domainType); + } + + private void deleteById(String containerName, Object id, PartitionKey partitionKey, + CosmosItemRequestOptions options) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + containerName = getContainerNameOverride(containerName); + String idToDelete = CosmosUtils.getStringIDValue(id); + LOGGER.debug("execute deleteById in database {} container {}", this.getDatabaseName(), + containerName); + + if (partitionKey == null) { + partitionKey = PartitionKey.NONE; + } + + this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .deleteItem(idToDelete, partitionKey, options) + .publishOn(Schedulers.parallel()) + .doOnNext(response -> + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + response.getDiagnostics(), null)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to delete item", + throwable, this.responseDiagnosticsProcessor)) + .block(); + } + + @Override + public Iterable findByIds(Iterable ids, Class domainType, String containerName) { + containerName = getContainerNameOverride(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 List idList = new ArrayList<>(); + for (ID id : ids) { + idList.add(CosmosUtils.getStringIDValue(id)); + } + final CosmosQuery query = new CosmosQuery(Criteria.getInstance(CriteriaType.IN, "id", + Collections.singletonList(idList), Part.IgnoreCaseType.NEVER)); + 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 Iterable find(@NonNull CosmosQuery 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, containerName, domainType); + } + + /** + * 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 CosmosQuery query, @NonNull Class domainType, + String containerName) { + return this.count(query, containerName) > 0; + } + + /** + * Delete the DocumentQuery, need to query the domains at first, then delete the item from the result. The cosmos db + * 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 Iterable delete(@NonNull CosmosQuery 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"); + String finalContainerName = getContainerNameOverride(containerName); + + final List results = findItemsAsFlux(query, finalContainerName, domainType).collectList().block(); + assert results != null; + return results.stream() + .map(item -> deleteItem(item, finalContainerName, domainType)) + .collect(Collectors.toList()); + } + + @Override + public Page findAll(Pageable pageable, Class domainType, String containerName) { + final CosmosQuery query = + new CosmosQuery(Criteria.getInstance(CriteriaType.ALL)).with(pageable); + if (pageable.getSort().isSorted()) { + query.with(pageable.getSort()); + } + + return paginationQuery(query, domainType, containerName); + } + + @Override + public Page runPaginationQuery(SqlQuerySpec querySpec, Pageable pageable, Class domainType, Class returnType) { + final String containerName = getContainerName(domainType); + final SqlQuerySpec sortedQuerySpec = NativeQueryGenerator.getInstance().generateSortedQuery(querySpec, pageable.getSort()); + final SqlQuerySpec countQuerySpec = NativeQueryGenerator.getInstance().generateCountQuery(querySpec); + return paginationQuery(sortedQuerySpec, countQuerySpec, pageable, + pageable.getSort(), returnType, containerName, Optional.empty()); + } + + @Override + public Page paginationQuery(CosmosQuery query, Class domainType, String containerName) { + containerName = getContainerNameOverride(containerName); + final SqlQuerySpec querySpec = new FindQuerySpecGenerator().generateCosmos(query); + final SqlQuerySpec countQuerySpec = new CountQueryGenerator().generateCosmos(query); + Optional partitionKeyValue = query.getPartitionKeyValue(domainType); + return paginationQuery(querySpec, countQuerySpec, query.getPageable(), + query.getSort(), domainType, containerName, partitionKeyValue); + } + + @Override + public Slice sliceQuery(CosmosQuery query, Class domainType, String containerName) { + containerName = getContainerNameOverride(containerName); + final SqlQuerySpec querySpec = new FindQuerySpecGenerator().generateCosmos(query); + Optional partitionKeyValue = query.getPartitionKeyValue(domainType); + return sliceQuery(querySpec, query.getPageable(), query.getSort(), domainType, containerName, partitionKeyValue); + } + + @Override + public Slice runSliceQuery(SqlQuerySpec querySpec, Pageable pageable, Class domainType, Class returnType) { + final String containerName = getContainerName(domainType); + final SqlQuerySpec sortedQuerySpec = NativeQueryGenerator.getInstance().generateSortedQuery(querySpec, pageable.getSort()); + return sliceQuery(sortedQuerySpec, pageable, pageable.getSort(), returnType, containerName, Optional.empty()); + } + + private Page paginationQuery(SqlQuerySpec querySpec, SqlQuerySpec countQuerySpec, + Pageable pageable, Sort sort, + Class returnType, String containerName, + Optional partitionKeyValue) { + containerName = getContainerNameOverride(containerName); + Slice response = sliceQuery(querySpec, pageable, sort, returnType, containerName, partitionKeyValue); + final long total = getCountValue(countQuerySpec, containerName); + return new CosmosPageImpl<>(response.getContent(), response.getPageable(), total); + } + + private Slice sliceQuery(SqlQuerySpec querySpec, + Pageable pageable, Sort sort, + Class returnType, String containerName, + Optional partitionKeyValue) { + Assert.isTrue(pageable.getPageSize() > 0, + "pageable should have page size larger than 0"); + Assert.hasText(containerName, "container should not be null, empty or only whitespaces"); + containerName = getContainerNameOverride(containerName); + final CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + cosmosQueryRequestOptions.setQueryMetricsEnabled(this.queryMetricsEnabled); + cosmosQueryRequestOptions.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + cosmosQueryRequestOptions.setMaxBufferedItemCount(this.maxBufferedItemCount); + cosmosQueryRequestOptions.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + partitionKeyValue.ifPresent(o -> { + LOGGER.debug("Setting partition key {}", o); + cosmosQueryRequestOptions.setPartitionKey(new PartitionKey(o)); + }); + + CosmosAsyncContainer container = + this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()).getContainer(containerName); + + Flux> feedResponseFlux; + /* + * The user can pass in an offset with the pageable, if this is done we need to apply + * the offset to the first page so that we can shift the data and skip the number of + * offset records. Starting with the 2nd page it picks up where the first page left off + * so we do not need to apply the offset as the continuation token handles the pages. + * + * We only use offset on the first page because of the use of continuation tokens. + * After we apply the offset to the first page, the continuation token will pick + * up the second and future pages at the correct index. + */ + int feedResponseContentSize = pageable.getPageSize(); + String continuationToken = null; + int offsetForPageWithoutContToken = 0; + if (pageable instanceof CosmosPageRequest) { + if (((CosmosPageRequest) pageable).getRequestContinuation() == null) { + feedResponseContentSize = (int) (feedResponseContentSize + + (feedResponseContentSize * pageable.getPageNumber()) + pageable.getOffset()); + offsetForPageWithoutContToken = (pageable.getPageNumber() * pageable.getPageSize()) + + (int) pageable.getOffset(); + } + continuationToken = ((CosmosPageRequest) pageable).getRequestContinuation(); + } else { + feedResponseContentSize = feedResponseContentSize + (feedResponseContentSize * pageable.getPageNumber()); + offsetForPageWithoutContToken = (pageable.getPageNumber() * pageable.getPageSize()); + } + + final List result = new ArrayList<>(); + do { + feedResponseFlux = container + .queryItems(querySpec, cosmosQueryRequestOptions, JsonNode.class) + .byPage(continuationToken, feedResponseContentSize); + FeedResponse feedResponse = feedResponseFlux + .publishOn(Schedulers.parallel()) + .doOnNext(propertiesFeedResponse -> + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + propertiesFeedResponse.getCosmosDiagnostics(), propertiesFeedResponse)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to query items", throwable, + this.responseDiagnosticsProcessor)) + .next() + .block(); + assert feedResponse != null; + Iterator it = feedResponse.getResults().iterator(); + + for (int index = 0; it.hasNext() + && index < pageable.getPageSize() + offsetForPageWithoutContToken; index++) { + + final JsonNode jsonNode = it.next(); + if (jsonNode == null) { + continue; + } + + if (index >= offsetForPageWithoutContToken) { + maybeEmitEvent(new AfterLoadEvent<>(jsonNode, returnType, containerName)); + final T entity = mappingCosmosConverter.read(returnType, jsonNode); + result.add(entity); + } + } + + if (result.size() < pageable.getPageSize()) { + feedResponseContentSize = pageable.getPageSize() - result.size(); + } + continuationToken = feedResponse.getContinuationToken(); + offsetForPageWithoutContToken = 0; + } while (result.size() < pageable.getPageSize() && continuationToken != null); + + final CosmosPageRequest pageRequest = CosmosPageRequest.of(pageable.getOffset(), + pageable.getPageNumber(), + pageable.getPageSize(), + continuationToken, + sort); + + return new CosmosSliceImpl<>(result, pageRequest, continuationToken != null); + } + + @Override + public long count(String containerName) { + Assert.hasText(containerName, "container name should not be empty"); + containerName = getContainerNameOverride(containerName); + final CosmosQuery query = new CosmosQuery(Criteria.getInstance(CriteriaType.ALL)); + final Long count = getCountValue(query, containerName); + assert count != null; + return count; + } + + @Override + public long count(CosmosQuery query, String containerName) { + containerName = getContainerNameOverride(containerName); + Assert.hasText(containerName, "container name should not be empty"); + + final Long count = getCountValue(query, containerName); + assert count != null; + return count; + } + + @Override + public long count(SqlQuerySpec querySpec, String containerName) { + containerName = getContainerNameOverride(containerName); + Assert.hasText(containerName, "container name should not be empty"); + + final Long count = getCountValue(querySpec, containerName); + assert count != null; + return count; + } + + @Override + public MappingCosmosConverter getConverter() { + return this.mappingCosmosConverter; + } + + @Override + public Iterable runQuery(SqlQuerySpec querySpec, Class domainType, Class returnType) { + return runQuery(querySpec, Sort.unsorted(), domainType, returnType); + } + + @Override + public Iterable runQuery(SqlQuerySpec querySpec, Sort sort, Class domainType, Class returnType) { + querySpec = NativeQueryGenerator.getInstance().generateSortedQuery(querySpec, sort); + return getJsonNodeFluxFromQuerySpec(getContainerName(domainType), querySpec) + .map(jsonNode -> emitOnLoadEventAndConvertToDomainObject(returnType, getContainerName(domainType), jsonNode)) + .collectList() + .block(); + } + + private void markAuditedIfConfigured(Object object) { + if (cosmosAuditingHandler != null) { + cosmosAuditingHandler.markAudited(object); + } + } + + private Long getCountValue(CosmosQuery query, String containerName) { + final SqlQuerySpec querySpec = new CountQueryGenerator().generateCosmos(query); + return getCountValue(querySpec, containerName); + } + + private Long getCountValue(SqlQuerySpec querySpec, String containerName) { + final CosmosQueryRequestOptions options = new CosmosQueryRequestOptions(); + options.setQueryMetricsEnabled(this.queryMetricsEnabled); + options.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + options.setMaxBufferedItemCount(this.maxBufferedItemCount); + options.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + containerName = getContainerNameOverride(containerName); + + return executeQuery(querySpec, containerName, options) + .publishOn(Schedulers.parallel()) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to get count value", throwable, + this.responseDiagnosticsProcessor)) + .doOnNext(response -> CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + response.getCosmosDiagnostics(), response)) + .next() + .map(r -> r.getResults().get(0).asLong()) + .block(); + } + + private Flux> executeQuery(SqlQuerySpec sqlQuerySpec, + String containerName, + CosmosQueryRequestOptions options) { + containerName = getContainerNameOverride(containerName); + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .queryItems(sqlQuerySpec, options, JsonNode.class) + .byPage(); + } + + private Flux findItemsAsFlux(@NonNull CosmosQuery query, + @NonNull String containerName, + @NonNull Class domainType) { + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + final CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + containerName = getContainerNameOverride(containerName); + cosmosQueryRequestOptions.setQueryMetricsEnabled(this.queryMetricsEnabled); + cosmosQueryRequestOptions.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + cosmosQueryRequestOptions.setMaxBufferedItemCount(this.maxBufferedItemCount); + cosmosQueryRequestOptions.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + Optional partitionKeyValue = query.getPartitionKeyValue(domainType); + partitionKeyValue.ifPresent(o -> { + LOGGER.debug("Setting partition key {}", o); + cosmosQueryRequestOptions.setPartitionKey(new PartitionKey(o)); + }); + + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .queryItems(sqlQuerySpec, cosmosQueryRequestOptions, JsonNode.class) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), + cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.getResults()); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to find items", throwable, + this.responseDiagnosticsProcessor)); + } + + private Flux getJsonNodeFluxFromQuerySpec( + @NonNull String containerName, SqlQuerySpec sqlQuerySpec) { + final CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + cosmosQueryRequestOptions.setQueryMetricsEnabled(this.queryMetricsEnabled); + cosmosQueryRequestOptions.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + cosmosQueryRequestOptions.setMaxBufferedItemCount(this.maxBufferedItemCount); + cosmosQueryRequestOptions.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + containerName = getContainerNameOverride(containerName); + + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .queryItems(sqlQuerySpec, cosmosQueryRequestOptions, JsonNode.class) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), + cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.getResults()); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to find items", throwable, + this.responseDiagnosticsProcessor)); + } + + private Iterable findItems(@NonNull CosmosQuery query, + @NonNull String containerName, + @NonNull Class domainType) { + String finalContainerName = getContainerNameOverride(containerName); + return findItemsAsFlux(query, finalContainerName, domainType) + .map(jsonNode -> emitOnLoadEventAndConvertToDomainObject(domainType, finalContainerName, jsonNode)) + .toIterable(); + } + + private T deleteItem(@NonNull JsonNode jsonNode, + String containerName, + @NonNull Class domainType) { + + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + applyVersioning(domainType, jsonNode, options); + containerName = getContainerNameOverride(containerName); + + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .deleteItem(jsonNode, options) + .publishOn(Schedulers.parallel()) + .doOnNext(response -> CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + response.getDiagnostics(), null)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to delete item", throwable, + this.responseDiagnosticsProcessor)) + .flatMap(objectCosmosItemResponse -> Mono.just(toDomainObject(domainType, jsonNode))) + .block(); + } + + private T emitOnLoadEventAndConvertToDomainObject(@NonNull Class domainType, String containerName, JsonNode responseJsonNode) { + containerName = getContainerNameOverride(containerName); + maybeEmitEvent(new AfterLoadEvent<>(responseJsonNode, domainType, containerName)); + return toDomainObject(domainType, responseJsonNode); + } + + private T toDomainObject(@NonNull Class domainType, JsonNode responseJsonNode) { + return mappingCosmosConverter.read(domainType, responseJsonNode); + } + + private void applyVersioning(Class domainType, + JsonNode jsonNode, + CosmosItemRequestOptions options) { + CosmosEntityInformation entityInformation = CosmosEntityInformation.getInstance(domainType); + if (entityInformation.isVersioned()) { + options.setIfMatchETag(jsonNode.get(Constants.ETAG_PROPERTY_DEFAULT_NAME).asText()); + } + } + + private void maybeEmitEvent(CosmosMappingEvent event) { + if (canPublishEvent()) { + this.applicationContext.publishEvent(event); + } + } + + private boolean canPublishEvent() { + return this.applicationContext != null; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java new file mode 100644 index 000000000000..f827271efcc3 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosContainerResponse; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.springframework.data.domain.Sort; +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); + + /** + * Creates a container if it doesn't already exist + * + * @param information the CosmosEntityInformation + * @return Mono of CosmosContainerResponse + */ + Mono createContainerIfNotExists(CosmosEntityInformation information); + + /** + * Get properties for specified container + * + * @param containerName String + * @return CosmosContainerProperties + */ + Mono getContainerProperties(String containerName); + + /** + * Replace container properties for the specified container + * + * @param containerName String + * @param properties CosmosContainerProperties + * @return CosmosContainerProperties + */ + Mono replaceContainerProperties(String containerName, + CosmosContainerProperties properties); + + /** + * Find all items in a given container + * + * @param containerName the containerName + * @param domainType the domainType + * @param type of domainType + * @return Flux of results + */ + Flux findAll(String containerName, Class domainType); + + /** + * Find all items in a given container + * + * @param domainType the domainType + * @param type of domainType + * @return Flux of results + */ + 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 of results + */ + Flux findAll(PartitionKey partitionKey, Class domainType); + + /** + * Find by id + * + * @param id the id + * @param domainType the domainType + * @param type of domainType + * @return Mono of result + */ + 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 of result + */ + 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 of result + */ + 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 of result + */ + 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 of result + */ + Mono insert(String containerName, T objectToSave, PartitionKey partitionKey); + + /** + * Insert + * + * @param containerName must not be {@literal null} + * @param objectToSave must not be {@literal null} + * @param type class of domain type + * @return Mono of result + */ + Mono insert(String containerName, T objectToSave); + + /** + * patches item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param type class of domain type + * @return the patched item + */ + Mono patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations); + + /** + * patches item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return Mono with the patched item + */ + Mono patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options); + + /** + * Upsert an item with partition key + * + * @param object the object to upsert + * @param type class of object + * @return Mono of result + */ + Mono upsert(T object); + + /** + * Upsert an item to container with partition key + * + * @param containerName the container name + * @param object the object to save + * @param type class of object + * @return Mono of result + */ + Mono upsert(String containerName, T object); + + /** + * 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 using entity + * + * @param type class of domain type + * @param containerName the container name + * @param entity the entity object + * @return void Mono + */ + Mono deleteEntity(String containerName, T entity); + + /** + * Delete all items in a container + * + * @param containerName the container name + * @param domainType the domainType + * @return void Mono + */ + Mono deleteAll(String containerName, Class domainType); + + /** + * 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 of results + */ + Flux delete(CosmosQuery 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 of results + */ + Flux find(CosmosQuery query, Class domainType, String containerName); + + /** + * Exists + * + * @param query the document query + * @param domainType type class + * @param containerName the container name + * @return Mono of result + */ + Mono exists(CosmosQuery query, Class domainType, String containerName); + + /** + * Exists + * @param id the id + * @param domainType type class + * @param containerName the container name + * @return Mono of result + */ + Mono existsById(Object id, Class domainType, String containerName); + + /** + * Count + * + * @param containerName the container name + * @return Mono of result + */ + Mono count(String containerName); + + /** + * Count + * + * @param query the document query + * @param containerName the container name + * @return Mono of result + */ + Mono count(CosmosQuery query, String containerName); + + /** + * Count + * + * @param querySpec the document query spec + * @param containerName the container name + * @return Mono of result + */ + Mono count(SqlQuerySpec querySpec, String containerName); + + /** + * To get converter + * @return MappingCosmosConverter + */ + MappingCosmosConverter getConverter(); + + /** + * Run the query. + * + * @param the type parameter + * @param querySpec the query spec + * @param domainType the domain type + * @param returnType the return type + * @return the flux + */ + Flux runQuery(SqlQuerySpec querySpec, Class domainType, Class returnType); + + /** + * Run the query. + * + * @param the type parameter + * @param querySpec the query spec + * @param sort the sort order + * @param domainType the domain type + * @param returnType the return type + * @return the flux + */ + Flux runQuery(SqlQuerySpec querySpec, Sort sort, Class domainType, Class returnType); + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java new file mode 100644 index 000000000000..6d580034ab1d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java @@ -0,0 +1,933 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosAsyncDatabase; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosContainerResponse; +import com.azure.cosmos.models.CosmosDatabaseResponse; +import com.azure.cosmos.models.CosmosItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.CosmosQueryRequestOptions; +import com.azure.cosmos.models.FeedResponse; +import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.cosmos.models.ThroughputProperties; +import com.azure.cosmos.models.UniqueKeyPolicy; +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.common.CosmosUtils; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.config.DatabaseThroughputConfig; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.generator.CountQueryGenerator; +import com.azure.spring.data.cosmos.core.generator.FindQuerySpecGenerator; +import com.azure.spring.data.cosmos.core.generator.NativeQueryGenerator; +import com.azure.spring.data.cosmos.core.mapping.event.AfterLoadEvent; +import com.azure.spring.data.cosmos.core.mapping.event.CosmosMappingEvent; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.exception.CosmosExceptionUtils; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.databind.JsonNode; +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.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.domain.Sort; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.util.Optional; +import java.util.UUID; + +/** + * Template class of reactive cosmos + */ +@SuppressWarnings("unchecked") +public class ReactiveCosmosTemplate implements ReactiveCosmosOperations, ApplicationContextAware { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveCosmosTemplate.class); + + private final CosmosFactory cosmosFactory; + private final MappingCosmosConverter mappingCosmosConverter; + private final ResponseDiagnosticsProcessor responseDiagnosticsProcessor; + private final boolean queryMetricsEnabled; + private final int maxDegreeOfParallelism; + private final int maxBufferedItemCount; + private final int responseContinuationTokenLimitInKb; + private final IsNewAwareAuditingHandler cosmosAuditingHandler; + private final DatabaseThroughputConfig databaseThroughputConfig; + + private ApplicationContext applicationContext; + + /** + * Initialization + * + * @param client must not be {@literal null} + * @param databaseName must not be {@literal null} + * @param cosmosConfig must not be {@literal null} + * @param mappingCosmosConverter must not be {@literal null} + * @param cosmosAuditingHandler can be {@literal null} + */ + public ReactiveCosmosTemplate(CosmosAsyncClient client, String databaseName, + CosmosConfig cosmosConfig, MappingCosmosConverter mappingCosmosConverter, + IsNewAwareAuditingHandler cosmosAuditingHandler) { + this(new CosmosFactory(client, databaseName), cosmosConfig, mappingCosmosConverter, cosmosAuditingHandler); + } + + /** + * Initialization + * + * @param client must not be {@literal null} + * @param databaseName must not be {@literal null} + * @param cosmosConfig must not be {@literal null} + * @param mappingCosmosConverter must not be {@literal null} + */ + public ReactiveCosmosTemplate(CosmosAsyncClient client, String databaseName, + CosmosConfig cosmosConfig, MappingCosmosConverter mappingCosmosConverter) { + this(new CosmosFactory(client, databaseName), cosmosConfig, mappingCosmosConverter, null); + } + + /** + * Constructor + * + * @param cosmosFactory the cosmos db factory + * @param cosmosConfig the cosmos config + * @param mappingCosmosConverter the mappingCosmosConverter + * @param cosmosAuditingHandler the auditing handler + */ + public ReactiveCosmosTemplate(CosmosFactory cosmosFactory, + CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter, + IsNewAwareAuditingHandler cosmosAuditingHandler) { + Assert.notNull(cosmosFactory, "CosmosFactory must not be null!"); + Assert.notNull(cosmosConfig, "CosmosConfig must not be null!"); + Assert.notNull(mappingCosmosConverter, "MappingCosmosConverter must not be null!"); + + this.mappingCosmosConverter = mappingCosmosConverter; + this.cosmosFactory = cosmosFactory; + this.responseDiagnosticsProcessor = cosmosConfig.getResponseDiagnosticsProcessor(); + this.queryMetricsEnabled = cosmosConfig.isQueryMetricsEnabled(); + this.maxDegreeOfParallelism = cosmosConfig.getMaxDegreeOfParallelism(); + this.maxBufferedItemCount = cosmosConfig.getMaxBufferedItemCount(); + this.responseContinuationTokenLimitInKb = cosmosConfig.getResponseContinuationTokenLimitInKb(); + this.cosmosAuditingHandler = cosmosAuditingHandler; + this.databaseThroughputConfig = cosmosConfig.getDatabaseThroughputConfig(); + } + + /** + * Initialization + * + * @param cosmosFactory must not be {@literal null} + * @param cosmosConfig must not be {@literal null} + * @param mappingCosmosConverter must not be {@literal null} + */ + public ReactiveCosmosTemplate(CosmosFactory cosmosFactory, + CosmosConfig cosmosConfig, + MappingCosmosConverter mappingCosmosConverter) { + this(cosmosFactory, cosmosConfig, mappingCosmosConverter, null); + } + + private String getDatabaseName() { + return this.cosmosFactory.getDatabaseName(); + } + + private CosmosAsyncClient getCosmosAsyncClient() { + return this.cosmosFactory.getCosmosAsyncClient(); + } + + /** + * @param applicationContext the application context + * @throws BeansException the bean exception + */ + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + /** + * Creates a container if it doesn't already exist + * + * @param information the CosmosEntityInformation + * @return Mono containing CosmosContainerResponse + */ + @Override + public Mono createContainerIfNotExists(CosmosEntityInformation information) { + + return createDatabaseIfNotExists() + .publishOn(Schedulers.parallel()) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to create database", throwable, + this.responseDiagnosticsProcessor)) + .flatMap(cosmosDatabaseResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosDatabaseResponse.getDiagnostics(), null); + final CosmosContainerProperties cosmosContainerProperties = + new CosmosContainerProperties(getContainerNameOverride(information.getContainerName()), information.getPartitionKeyPath()); + cosmosContainerProperties.setDefaultTimeToLiveInSeconds(information.getTimeToLive()); + cosmosContainerProperties.setIndexingPolicy(information.getIndexingPolicy()); + final UniqueKeyPolicy uniqueKeyPolicy = information.getUniqueKeyPolicy(); + if (uniqueKeyPolicy != null) { + cosmosContainerProperties.setUniqueKeyPolicy(uniqueKeyPolicy); + } + + CosmosAsyncDatabase database = + this.getCosmosAsyncClient().getDatabase(cosmosDatabaseResponse.getProperties().getId()); + Mono cosmosContainerResponseMono; + + if (information.getRequestUnit() == null) { + cosmosContainerResponseMono = + database.createContainerIfNotExists(cosmosContainerProperties); + } else { + ThroughputProperties throughputProperties = information.isAutoScale() + ? ThroughputProperties.createAutoscaledThroughput(information.getRequestUnit()) + : ThroughputProperties.createManualThroughput(information.getRequestUnit()); + cosmosContainerResponseMono = + database.createContainerIfNotExists(cosmosContainerProperties, + throughputProperties); + } + + return cosmosContainerResponseMono + .map(cosmosContainerResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosContainerResponse.getDiagnostics(), null); + return cosmosContainerResponse; + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to create container", + throwable, this.responseDiagnosticsProcessor)); + }); + + } + + private Mono createDatabaseIfNotExists() { + if (databaseThroughputConfig == null) { + return this.getCosmosAsyncClient() + .createDatabaseIfNotExists(this.getDatabaseName()); + } else { + ThroughputProperties throughputProperties = databaseThroughputConfig.isAutoScale() + ? ThroughputProperties.createAutoscaledThroughput(databaseThroughputConfig.getRequestUnits()) + : ThroughputProperties.createManualThroughput(databaseThroughputConfig.getRequestUnits()); + return this.getCosmosAsyncClient() + .createDatabaseIfNotExists(this.getDatabaseName(), throughputProperties); + } + } + + @Override + public Mono getContainerProperties(String containerName) { + containerName = getContainerNameOverride(containerName); + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .read() + .map(CosmosContainerResponse::getProperties); + } + + @Override + public Mono replaceContainerProperties(String containerName, + CosmosContainerProperties properties) { + containerName = getContainerNameOverride(containerName); + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .replace(properties) + .map(CosmosContainerResponse::getProperties); + } + + /** + * + * 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 CosmosQuery query = new CosmosQuery(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 CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + cosmosQueryRequestOptions.setPartitionKey(partitionKey); + cosmosQueryRequestOptions.setQueryMetricsEnabled(this.queryMetricsEnabled); + cosmosQueryRequestOptions.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + cosmosQueryRequestOptions.setMaxBufferedItemCount(this.maxBufferedItemCount); + cosmosQueryRequestOptions.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .queryItems("SELECT * FROM r", cosmosQueryRequestOptions, JsonNode.class) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.getResults()); + }) + .map(cosmosItemProperties -> emitOnLoadEventAndConvertToDomainObject(domainType, containerName, cosmosItemProperties)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to find items", throwable, + this.responseDiagnosticsProcessor)); + } + + /** + * 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 container name + * @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"); + final String finalContainerName = getContainerNameOverride(containerName); + final String query = "select * from root where root.id = @ROOT_ID"; + final SqlParameter param = new SqlParameter("@ROOT_ID", CosmosUtils.getStringIDValue(id)); + final SqlQuerySpec sqlQuerySpec = new SqlQuerySpec(query, param); + final CosmosQueryRequestOptions options = new CosmosQueryRequestOptions(); + options.setQueryMetricsEnabled(this.queryMetricsEnabled); + options.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + options.setMaxBufferedItemCount(this.maxBufferedItemCount); + options.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(finalContainerName) + .queryItems(sqlQuerySpec, options, JsonNode.class) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), + cosmosItemFeedResponse); + return Mono.justOrEmpty(cosmosItemFeedResponse + .getResults() + .stream() + .map(cosmosItem -> emitOnLoadEventAndConvertToDomainObject(domainType, finalContainerName, cosmosItem)) + .findFirst()); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.findAPIExceptionHandler("Failed to find item", throwable, + this.responseDiagnosticsProcessor)) + .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"); + String idToFind = CosmosUtils.getStringIDValue(id); + + final String containerName = getContainerName(domainType); + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .readItem(idToFind, partitionKey, JsonNode.class) + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null); + return Mono.justOrEmpty(emitOnLoadEventAndConvertToDomainObject(domainType, + containerName, cosmosItemResponse.getItem())); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.findAPIExceptionHandler("Failed to find item", throwable, + this.responseDiagnosticsProcessor)); + } + + /** + * 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) { + 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) { + return insert(getContainerName(objectToSave.getClass()), objectToSave, null); + } + + /** + * 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, T objectToSave, + PartitionKey partitionKey) { + Assert.hasText(containerName, "containerName should not be null, empty or only whitespaces"); + Assert.notNull(objectToSave, "objectToSave should not be null"); + containerName = getContainerNameOverride(containerName); + final Class domainType = (Class) objectToSave.getClass(); + markAuditedIfConfigured(objectToSave); + generateIdIfNullAndAutoGenerationEnabled(objectToSave, domainType); + final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(objectToSave); + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + // if the partition key is null, SDK will get the partitionKey from the object + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .createItem(originalItem, partitionKey, options) + .publishOn(Schedulers.parallel()) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to insert item", throwable, + this.responseDiagnosticsProcessor)) + .flatMap(cosmosItemResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null); + return Mono.just(toDomainObject(domainType, cosmosItemResponse.getItem())); + }); + } + + /** + * Insert + * + * @param type of inserted objectToSave + * @param containerName the container name + * @param objectToSave the object to save + * @return Mono with the item or error + */ + @Override + public Mono insert(String containerName, T objectToSave) { + return insert(containerName, objectToSave, null); + } + + /** + * Patches item + * + * applies partial update (patch) to an item + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null} + * @param type class of domain type + * @return the patched item + */ + @Override + public Mono patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations) { + return patch(id, partitionKey, domainType, patchOperations, null); + } + + /** + * applies partial update (patch) to an item with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null} + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched item + */ + public Mono patch(Object id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options) { + Assert.notNull(patchOperations, "expected non-null cosmosPatchOperations"); + + final String containerName = getContainerName(domainType); + Assert.notNull(id, "id should not be null"); + Assert.notNull(partitionKey, "partitionKey should not be null, empty or only whitespaces"); + Assert.notNull(patchOperations, "patchOperations should not be null, empty or only whitespaces"); + + LOGGER.debug("execute patchItem in database {} container {}", this.getDatabaseName(), containerName); + + if (options == null) { + options = new CosmosPatchItemRequestOptions(); + } + + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .patchItem(id.toString(), partitionKey, patchOperations, options, JsonNode.class) + .publishOn(Schedulers.parallel()) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to patch item", throwable, + this.responseDiagnosticsProcessor)) + .flatMap(cosmosItemResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null); + return Mono.just(toDomainObject(domainType, cosmosItemResponse.getItem())); + }); + + } + + @SuppressWarnings("unchecked") + private void generateIdIfNullAndAutoGenerationEnabled(T originalItem, Class type) { + CosmosEntityInformation entityInfo = CosmosEntityInformation.getInstance(type); + if (entityInfo.shouldGenerateId() && ReflectionUtils.getField(entityInfo.getIdField(), originalItem) == null) { + ReflectionUtils.setField(entityInfo.getIdField(), originalItem, UUID.randomUUID().toString()); + } + } + + /** + * Upsert + * + * @param object the object to upsert + * @return Mono with the item or error + */ + @Override + public Mono upsert(T object) { + return upsert(getContainerName(object.getClass()), object); + } + + /** + * Upsert + * + * @param containerName the container name + * @param object the object to save + * @return Mono with the item or error + */ + @Override + public Mono upsert(String containerName, T object) { + containerName = getContainerNameOverride(containerName); + final Class domainType = (Class) object.getClass(); + markAuditedIfConfigured(object); + final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(object); + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + + applyVersioning(object.getClass(), originalItem, options); + + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .upsertItem(originalItem, options) + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null); + return Mono.just(toDomainObject(domainType, + cosmosItemResponse.getItem())); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to upsert item", throwable, + this.responseDiagnosticsProcessor)); + } + + /** + * Deletes the item with id and partition key. + * + * @param containerName Container name of database + * @param id item id + * @param partitionKey the partition key + */ + @Override + public Mono deleteById(String containerName, Object id, PartitionKey partitionKey) { + return deleteById(containerName, id, partitionKey, new CosmosItemRequestOptions()); + } + + private Mono deleteById(String containerName, Object id, PartitionKey partitionKey, + CosmosItemRequestOptions cosmosItemRequestOptions) { + containerName = getContainerNameOverride(containerName); + Assert.hasText(containerName, "container name should not be null, empty or only whitespaces"); + String idToDelete = CosmosUtils.getStringIDValue(id); + + if (partitionKey == null) { + partitionKey = PartitionKey.NONE; + } + + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .deleteItem(idToDelete, partitionKey, cosmosItemRequestOptions) + .publishOn(Schedulers.parallel()) + .doOnNext(cosmosItemResponse -> + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to delete item", throwable, + this.responseDiagnosticsProcessor)) + .then(); + } + + /** + * Deletes the entity + * + * @param type class of domain type + * @param containerName Container name of database + * @param entity the entity to delete + * @return void Mono + */ + public Mono deleteEntity(String containerName, T entity) { + Assert.notNull(entity, "entity to be deleted should not be null"); + @SuppressWarnings("unchecked") + final Class domainType = (Class) entity.getClass(); + final JsonNode originalItem = mappingCosmosConverter.writeJsonNode(entity); + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + applyVersioning(entity.getClass(), originalItem, options); + return deleteItem(originalItem, containerName, domainType).then(); + } + + /** + * Delete all items in a container + * + * @param containerName the container name + * @param domainType the domainType + * @return void Mono + */ + @Override + public Mono deleteAll(@NonNull String containerName, @NonNull Class domainType) { + Assert.hasText(containerName, "container name should not be null, empty or only whitespaces"); + + final CosmosQuery query = new CosmosQuery(Criteria.getInstance(CriteriaType.ALL)); + + return this.delete(query, domainType, containerName).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(CosmosQuery query, Class domainType, String containerName) { + String finalContainerName = getContainerNameOverride(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, finalContainerName, domainType); + + return results.flatMap(d -> deleteItem(d, finalContainerName, domainType)); + } + + /** + * 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(CosmosQuery query, Class domainType, String containerName) { + return findItems(query, containerName, domainType) + .map(cosmosItemProperties -> emitOnLoadEventAndConvertToDomainObject(domainType, containerName, 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(CosmosQuery query, Class domainType, String containerName) { + return count(query, containerName).flatMap(count -> Mono.just(count > 0)); + } + + /** + * Exists + * + * @param id the id + * @param domainType the entity class + * @param containerName the container name + * @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 CosmosQuery query = new CosmosQuery(Criteria.getInstance(CriteriaType.ALL)); + return count(query, containerName); + } + + /** + * Count + * + * @param query the document query + * @param containerName the container name + * @return Mono with count or error + */ + @Override + public Mono count(CosmosQuery query, String containerName) { + final SqlQuerySpec querySpec = new CountQueryGenerator().generateCosmos(query); + return getCountValue(querySpec, containerName); + } + + /** + * Count + * + * @param querySpec the document query spec + * @param containerName the container name + * @return Mono with count or error + */ + @Override + public Mono count(SqlQuerySpec querySpec, String containerName) { + return getCountValue(querySpec, containerName); + } + + @Override + public MappingCosmosConverter getConverter() { + return mappingCosmosConverter; + } + + @Override + public Flux runQuery(SqlQuerySpec querySpec, Class domainType, Class returnType) { + return runQuery(querySpec, Sort.unsorted(), domainType, returnType); + } + + @Override + public Flux runQuery(SqlQuerySpec querySpec, Sort sort, Class domainType, Class returnType) { + SqlQuerySpec sortedQuerySpec = NativeQueryGenerator.getInstance().generateSortedQuery(querySpec, sort); + return runQuery(sortedQuerySpec, domainType) + .map(cosmosItemProperties -> emitOnLoadEventAndConvertToDomainObject(returnType, getContainerName(domainType), cosmosItemProperties)); + } + + private Flux runQuery(SqlQuerySpec querySpec, Class domainType) { + String containerName = getContainerName(domainType); + CosmosQueryRequestOptions options = new CosmosQueryRequestOptions(); + options.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + options.setMaxBufferedItemCount(this.maxBufferedItemCount); + options.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .queryItems(querySpec, options, JsonNode.class) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils + .fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), + cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.getResults()); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to find items", throwable, + this.responseDiagnosticsProcessor)); + } + + private Mono getCountValue(SqlQuerySpec querySpec, String containerName) { + final CosmosQueryRequestOptions options = new CosmosQueryRequestOptions(); + options.setQueryMetricsEnabled(this.queryMetricsEnabled); + options.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + options.setMaxBufferedItemCount(this.maxBufferedItemCount); + options.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + + return executeQuery(querySpec, containerName, options) + .doOnNext(feedResponse -> CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + feedResponse.getCosmosDiagnostics(), feedResponse)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to get count value", throwable, + this.responseDiagnosticsProcessor)) + .next() + .map(r -> r.getResults().get(0).asLong()); + } + + private Flux> executeQuery(SqlQuerySpec sqlQuerySpec, + String containerName, + CosmosQueryRequestOptions options) { + containerName = getContainerNameOverride(containerName); + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .queryItems(sqlQuerySpec, options, JsonNode.class) + .byPage() + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to execute query", throwable, + this.responseDiagnosticsProcessor)); + } + + /** + * Delete container with container name + * + * @param containerName the container name + */ + @Override + public void deleteContainer(@NonNull String containerName) { + containerName = getContainerNameOverride(containerName); + Assert.hasText(containerName, "containerName should have text."); + this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .delete() + .doOnNext(cosmosContainerResponse -> + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosContainerResponse.getDiagnostics(), null)) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to delete container", + throwable, this.responseDiagnosticsProcessor)) + .block(); + } + + @Override + public String getContainerName(Class domainType) { + Assert.notNull(domainType, "domainType should not be null"); + return getContainerNameOverride(CosmosEntityInformation.getInstance(domainType).getContainerName()); + } + + /** + * Check if an overridden version of containerName is present, and if so, return it + * + * @param containerName Container name of database + * @return containerName + */ + public String getContainerNameOverride(String containerName) { + if (this.cosmosFactory.overrideContainerName() != null) { + return this.cosmosFactory.overrideContainerName(); + } + Assert.notNull(containerName, "containerName should not be null"); + return containerName; + } + + private void markAuditedIfConfigured(Object object) { + if (cosmosAuditingHandler != null) { + cosmosAuditingHandler.markAudited(object); + } + } + + private Flux findItems(@NonNull CosmosQuery query, + @NonNull String containerName, + @NonNull Class domainType) { + containerName = getContainerNameOverride(containerName); + final SqlQuerySpec sqlQuerySpec = new FindQuerySpecGenerator().generateCosmos(query); + final CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + cosmosQueryRequestOptions.setQueryMetricsEnabled(this.queryMetricsEnabled); + cosmosQueryRequestOptions.setMaxDegreeOfParallelism(this.maxDegreeOfParallelism); + cosmosQueryRequestOptions.setMaxBufferedItemCount(this.maxBufferedItemCount); + cosmosQueryRequestOptions.setResponseContinuationTokenLimitInKb(this.responseContinuationTokenLimitInKb); + Optional partitionKeyValue = query.getPartitionKeyValue(domainType); + partitionKeyValue.ifPresent(o -> { + LOGGER.debug("Setting partition key {}", o); + cosmosQueryRequestOptions.setPartitionKey(new PartitionKey(o)); + }); + + return this.getCosmosAsyncClient() + .getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .queryItems(sqlQuerySpec, cosmosQueryRequestOptions, JsonNode.class) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.getResults()); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to query items", throwable, + this.responseDiagnosticsProcessor)); + } + + private Mono deleteItem(@NonNull JsonNode jsonNode, + String containerName, + @NonNull Class domainType) { + containerName = getContainerNameOverride(containerName); + final CosmosItemRequestOptions options = new CosmosItemRequestOptions(); + applyVersioning(domainType, jsonNode, options); + + return this.getCosmosAsyncClient().getDatabase(this.getDatabaseName()) + .getContainer(containerName) + .deleteItem(jsonNode, options) + .publishOn(Schedulers.parallel()) + .map(cosmosItemResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemResponse.getDiagnostics(), null); + return cosmosItemResponse; + }) + .flatMap(objectCosmosItemResponse -> Mono.just(toDomainObject(domainType, jsonNode))) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to delete item", throwable, + this.responseDiagnosticsProcessor)); + } + + private T emitOnLoadEventAndConvertToDomainObject(@NonNull Class domainType, String containerName, JsonNode responseJsonNode) { + containerName = getContainerNameOverride(containerName); + maybeEmitEvent(new AfterLoadEvent<>(responseJsonNode, domainType, containerName)); + return toDomainObject(domainType, responseJsonNode); + } + + private T toDomainObject(@NonNull Class domainType, JsonNode jsonNode) { + return mappingCosmosConverter.read(domainType, jsonNode); + } + + private void applyVersioning(Class domainType, + JsonNode jsonNode, + CosmosItemRequestOptions options) { + CosmosEntityInformation entityInformation = CosmosEntityInformation.getInstance(domainType); + if (entityInformation.isVersioned()) { + options.setIfMatchETag(jsonNode.get(Constants.ETAG_PROPERTY_DEFAULT_NAME).asText()); + } + } + + private void maybeEmitEvent(CosmosMappingEvent event) { + if (canPublishEvent()) { + this.applicationContext.publishEvent(event); + } + } + + private boolean canPublishEvent() { + return this.applicationContext != null; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ResponseDiagnostics.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ResponseDiagnostics.java new file mode 100644 index 000000000000..39d87fd1fa55 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ResponseDiagnostics.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosDiagnostics; +import com.azure.cosmos.models.FeedResponse; + +/** + * Diagnostics class of cosmos and feed response + *

+ * NOTE: activityId will be null in case of cross partition queries + */ +public class ResponseDiagnostics { + + private final CosmosDiagnostics cosmosDiagnostics; + private final CosmosResponseStatistics cosmosResponseStatistics; + + /** + * Initialization + * + * @param cosmosDiagnostics cannot be null + * @param cosmosResponseStatistics cannot be null + */ + public ResponseDiagnostics(CosmosDiagnostics cosmosDiagnostics, + CosmosResponseStatistics cosmosResponseStatistics) { + this.cosmosDiagnostics = cosmosDiagnostics; + this.cosmosResponseStatistics = cosmosResponseStatistics; + } + + /** + * To get diagnostics of cosmos response + * + * @return CosmosResponseDiagnostics + */ + public CosmosDiagnostics getCosmosDiagnostics() { + return cosmosDiagnostics; + } + + /** + * To get the statistics value of cosmos response + * + * @return CosmosResponseStatistics + */ + public CosmosResponseStatistics getCosmosResponseStatistics() { + return cosmosResponseStatistics; + } + + @Override + public String toString() { + final StringBuilder diagnostics = new StringBuilder(); + if (cosmosDiagnostics != null) { + diagnostics.append("cosmosResponseDiagnostics={") + .append(cosmosDiagnostics) + .append("}"); + } + if (cosmosResponseStatistics != null) { + if (diagnostics.length() != 0) { + diagnostics.append(", "); + } + diagnostics.append("cosmosResponseStatistics={") + .append(cosmosResponseStatistics) + .append("}"); + } + return diagnostics.toString(); + } + + /** + * Generates statistics from cosmos response + *

+ * NOTE: activityId will be null in case of cross partition queries + */ + 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.getRequestCharge(); + this.activityId = feedResponse.getActivityId(); + } + + /** + * 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ResponseDiagnosticsProcessor.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ResponseDiagnosticsProcessor.java new file mode 100644 index 000000000000..15e2c09e7543 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/ResponseDiagnosticsProcessor.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import org.springframework.lang.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. + *

+ * NOTE: Since processResponseDiagnostics() API will get called in every cosmos spring data implementation API to + * capture the diagnostics details, it is highly recommended to not have any long running / CPU intensive work in + * the implementation of this API. + * + * @param responseDiagnostics responseDiagnostics object containing CosmosDb response diagnostics information + */ + void processResponseDiagnostics(@Nullable ResponseDiagnostics responseDiagnostics); +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/MappingCosmosConverter.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/MappingCosmosConverter.java new file mode 100644 index 000000000000..e683e245a009 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/MappingCosmosConverter.java @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.convert; + +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.core.mapping.CosmosPersistentEntity; +import com.azure.spring.data.cosmos.core.mapping.CosmosPersistentProperty; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +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.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +import static com.azure.spring.data.cosmos.Constants.ISO_8601_COMPATIBLE_DATE_PATTERN; + +/** + * A converter class between common types and cosmosItemProperties + */ +public class MappingCosmosConverter + implements EntityConverter, CosmosPersistentProperty, Object, + JsonNode>, + ApplicationContextAware { + + /** + * Mapping context + */ + protected final MappingContext, CosmosPersistentProperty> mappingContext; + + /** + * Generic conversion service + */ + 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.OBJECT_MAPPER_BEAN_NAME) ObjectMapper objectMapper) { + this.mappingContext = mappingContext; + this.conversionService = new GenericConversionService(); + this.objectMapper = objectMapper == null ? ObjectMapperFactory.getObjectMapper() + : objectMapper; + } + + @Override + public R read(Class type, JsonNode jsonNode) { + final CosmosPersistentEntity entity = mappingContext.getPersistentEntity(type); + return readInternal(entity, type, jsonNode); + } + + @Override + public void write(Object source, JsonNode sink) { + throw new UnsupportedOperationException("The feature is not implemented yet"); + } + + private R readInternal(final CosmosPersistentEntity entity, Class type, + final JsonNode jsonNode) { + try { + if (jsonNode.isValueNode()) { + return objectMapper.treeToValue(jsonNode, type); + } + + Assert.notNull(entity, "Entity is null."); + final ObjectNode objectNode = jsonNode.deepCopy(); + final CosmosPersistentProperty idProperty = entity.getIdProperty(); + final JsonNode idValue = jsonNode.get("id"); + if (idProperty != null) { + // Replace the key id to the actual id field name in domain + objectNode.remove(Constants.ID_PROPERTY_NAME); + objectNode.set(idProperty.getName(), idValue); + } + final JsonNode etag = jsonNode.get(Constants.ETAG_PROPERTY_DEFAULT_NAME); + if (etag != null) { + mapEtagToVersionField(type, objectNode, etag); + } + return objectMapper.treeToValue(objectNode, type); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Failed to read the source document " + + jsonNode.toPrettyString() + + " to target type " + + type, e); + } + } + + /** + * 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 CosmosAccessException fail to map document value + */ + public JsonNode writeJsonNode(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 ObjectNode cosmosObjectNode; + + try { + final String valueAsString = objectMapper.writeValueAsString(sourceEntity); + cosmosObjectNode = (ObjectNode) objectMapper.readTree(valueAsString); + } catch (JsonProcessingException e) { + throw new CosmosAccessException("Failed to map document value.", e); + } + + if (idProperty != null) { + final Object value = accessor.getProperty(idProperty); + final String id = value == null ? null : value.toString(); + cosmosObjectNode.put("id", id); + } + + mapVersionFieldToEtag(sourceEntity, cosmosObjectNode); + + return cosmosObjectNode; + } + + //the field on the underlying cosmos document will always be _etag, so we map the field that the + //user has marked with @version to _etag and remove the @version annotated field from the + //object if the field is not named _etag + private void mapVersionFieldToEtag(Object sourceEntity, ObjectNode cosmosObjectNode) { + final CosmosEntityInformation entityInfo = CosmosEntityInformation.getInstance(sourceEntity.getClass()); + if (entityInfo.isVersioned()) { + if (!entityInfo.getVersionFieldName().equals(Constants.ETAG_PROPERTY_DEFAULT_NAME)) { + cosmosObjectNode.remove(entityInfo.getVersionFieldName()); + cosmosObjectNode.put(Constants.ETAG_PROPERTY_DEFAULT_NAME, + entityInfo.getVersionFieldValue(sourceEntity)); + } + } + } + + private void mapEtagToVersionField(Class type, ObjectNode objectNode, JsonNode etagValue) { + final CosmosEntityInformation entityInfo = CosmosEntityInformation.getInstance(type); + if (entityInfo.isVersioned()) { + objectNode.set(entityInfo.getVersionFieldName(), etagValue); + if (!entityInfo.getVersionFieldName().equals(Constants.ETAG_PROPERTY_DEFAULT_NAME)) { + objectNode.remove(Constants.ETAG_PROPERTY_DEFAULT_NAME); + } + } + } + + /** + * 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/ObjectMapperFactory.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/ObjectMapperFactory.java new file mode 100644 index 000000000000..0a17f100a468 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/ObjectMapperFactory.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/convert/package-info.java new file mode 100644 index 000000000000..5218f5599bbd --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.core.convert; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/AbstractQueryGenerator.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/AbstractQueryGenerator.java new file mode 100644 index 000000000000..ca9d9047a47e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/AbstractQueryGenerator.java @@ -0,0 +1,321 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.generator; + +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.exception.IllegalQueryException; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.util.Pair; +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.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter.toCosmosDbValue; + +/** + * Base class for generating sql query + */ +public abstract class AbstractQueryGenerator { + + private static String tableName = ""; + + private static final String CONSTANT_TABLE_NAME = "r"; + + /** + * Initialization + */ + protected AbstractQueryGenerator() { + } + + private String generateQueryParameter(@NonNull String subject, int counter) { + // user.name, user['name'] or user["first name"] are not valid sql parameter identifiers. + return subject.replaceAll("[^a-zA-Z\\d]", "_") + counter; + } + + 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, int counter) { + 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, counter); + final Part.IgnoreCaseType ignoreCase = criteria.getIgnoreCase(); + final String sqlKeyword = criteria.getType().getSqlKeyword(); + parameters.add(Pair.of(parameter, subjectValue)); + + if (CriteriaType.isFunction(criteria.getType())) { + return getFunctionCondition(ignoreCase, sqlKeyword, subject, parameter, + CriteriaType.isFunctionWithCaseSensitiveSupport(criteria.getType())); + } else if (criteria.getType() == CriteriaType.IS_EQUAL + && ignoreCase != Part.IgnoreCaseType.NEVER + && subjectValue instanceof String) { + return getFunctionCondition(ignoreCase, CriteriaType.STRING_EQUALS.getSqlKeyword(), + subject, parameter, true); + } else { + return getCondition(ignoreCase, sqlKeyword, subject, parameter); + } + } + + /** + * Get condition string with function + * + * @param ignoreCase ignore case flag + * @param sqlKeyword sql key word, operation name + * @param subject sql column name + * @param parameter sql filter value + * @return condition string + */ + private String getCondition(final Part.IgnoreCaseType ignoreCase, final String sqlKeyword, + final String subject, final String parameter) { + if (Part.IgnoreCaseType.NEVER == ignoreCase) { + return String.format("r.%s %s @%s", subject, sqlKeyword, parameter); + } else { + return String.format("UPPER(r.%s) %s UPPER(@%s)", subject, sqlKeyword, parameter); + } + } + + /** + * Get condition string without function + * + * @param ignoreCase ignore case flag + * @param sqlKeyword sql key word, operation name + * @param subject sql column name + * @param parameter sql filter value + * @param takesCaseSensitiveParam if the function type can take the third boolean param + * @return condition string + */ + private String getFunctionCondition(final Part.IgnoreCaseType ignoreCase, final String sqlKeyword, + final String subject, final String parameter, final boolean takesCaseSensitiveParam) { + if (Part.IgnoreCaseType.NEVER == ignoreCase) { + return String.format("%s(r.%s, @%s)", sqlKeyword, subject, parameter); + } else { + if (takesCaseSensitiveParam) { + return String.format("%s(r.%s, @%s, true)", sqlKeyword, subject, parameter); + } else { + return String.format("%s(UPPER(r.%s), UPPER(@%s))", sqlKeyword, subject, parameter); + } + } + } + + private String generateBetween(@NonNull Criteria criteria, @NonNull List> parameters, int counter) { + 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, counter); + final String parameter2 = generateQueryParameter(subject2, counter); + final String keyword = criteria.getType().getSqlKeyword(); + + parameters.add(Pair.of(parameter1, value1)); + parameters.add(Pair.of(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.format("(%s %s %s)", left, type.getSqlKeyword(), right); + } + + @SuppressWarnings("unchecked") + private String generateInQuery(@NonNull Criteria criteria, @NonNull List> parameters) { + 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 Collection values = (Collection) criteria.getSubjectValues().get(0); + + final List paras = new ArrayList<>(); + for (Object o : values) { + if (o instanceof String || o instanceof Integer || o instanceof Long || o instanceof Boolean) { + String key = "p" + parameters.size(); + paras.add("@" + key); + parameters.add(Pair.of(key, o)); + } else { + throw new IllegalQueryException("IN keyword Range only support Number and String type."); + } + } + + return String.format("r.%s %s (%s)", criteria.getSubject(), criteria.getType().getSqlKeyword(), + String.join(",", paras)); + } + + private String generateQueryBody(@NonNull Criteria criteria, @NonNull List> parameters, @NonNull final AtomicInteger counter) { + final CriteriaType type = criteria.getType(); + + switch (type) { + case ALL: + return ""; + case IN: + case NOT_IN: + return generateInQuery(criteria, parameters); + case BETWEEN: + return generateBetween(criteria, parameters, counter.getAndIncrement()); + 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 NOT_CONTAINING: + case ENDS_WITH: + case STARTS_WITH: + case ARRAY_CONTAINS: + case STRING_EQUALS: + return generateBinaryQuery(criteria, parameters, counter.getAndIncrement()); + case AND: + case OR: + Assert.isTrue(criteria.getSubCriteria().size() == 2, + "criteria should have two SubCriteria"); + + final String left = generateQueryBody(criteria.getSubCriteria().get(0), parameters, counter); + final String right = generateQueryBody(criteria.getSubCriteria().get(1), parameters, counter); + + 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 CosmosQuery query, @NonNull final AtomicInteger counter) { + final List> parameters = new ArrayList<>(); + String queryString = this.generateQueryBody(query.getCriteria(), parameters, counter); + + if (StringUtils.hasText(queryString)) { + queryString = String.join(" ", "WHERE", queryString); + } + + return Pair.of(queryString, parameters); + } + + private static 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("%s.%s %s", tableName, order.getProperty(), direction); + } + + static String generateQuerySort(@NonNull Sort sort, @NonNull String inputTableName) { + if (sort.isUnsorted()) { + return ""; + } + tableName = inputTableName; + + final String queryTail = "ORDER BY"; + final List subjects = sort.stream().map(AbstractQueryGenerator::getParameter).collect(Collectors.toList()); + + return queryTail + + " " + + String.join(",", subjects); + } + + @NonNull + private String generateQueryTail(@NonNull CosmosQuery query) { + final List queryTails = new ArrayList<>(); + + queryTails.add(generateQuerySort(query.getSort(), CONSTANT_TABLE_NAME)); + + return String.join(" ", queryTails.stream().filter(StringUtils::hasText).collect(Collectors.toList())); + } + + /** + * Generates a Cosmos count query. + * + * @param query the representation for query method. + * @param queryHead the query head. + * @return the SQL query spec. + */ + protected SqlQuerySpec generateCosmosCountQuery(@NonNull CosmosQuery query, + @NonNull String queryHead) { + final AtomicInteger counter = new AtomicInteger(); + final Pair>> queryBody = generateQueryBody(query, counter); + String queryString = String.join(" ", queryHead, queryBody.getFirst(), generateQueryTail(query)); + final List> parameters = queryBody.getSecond(); + + List sqlParameters = parameters.stream() + .map(p -> new SqlParameter("@" + p.getFirst(), + toCosmosDbValue(p.getSecond()))) + .collect(Collectors.toList()); + + return new SqlQuerySpec(queryString, sqlParameters); + } + + + /** + * Generates a Cosmos query. + * + * @param query the representation for query method. + * @param queryHead the query head. + * @return the SQL query spec. + */ + protected SqlQuerySpec generateCosmosQuery(@NonNull CosmosQuery query, + @NonNull String queryHead) { + final AtomicInteger counter = new AtomicInteger(); + final Pair>> queryBody = generateQueryBody(query, counter); + String queryString = String.join(" ", queryHead, queryBody.getFirst(), generateQueryTail(query)); + final List> parameters = queryBody.getSecond(); + + List sqlParameters = parameters.stream() + .map(p -> new SqlParameter("@" + p.getFirst(), + toCosmosDbValue(p.getSecond()))) + .collect(Collectors.toList()); + + if (query.getLimit() > 0) { + queryString = new StringBuilder(queryString) + .append(" OFFSET ") + .append(query.getOffset()) + .append(" LIMIT ") + .append(query.getLimit()).toString(); + } + + return new SqlQuerySpec(queryString, sqlParameters); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/CountQueryGenerator.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/CountQueryGenerator.java new file mode 100644 index 000000000000..5540a7b5dda9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/CountQueryGenerator.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.generator; + +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; + +/** + * Generate count query + */ +public class CountQueryGenerator extends AbstractQueryGenerator implements QuerySpecGenerator { + + @Override + public SqlQuerySpec generateCosmos(CosmosQuery query) { + return super.generateCosmosCountQuery(query, "SELECT VALUE COUNT(1) FROM r"); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/FindQuerySpecGenerator.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/FindQuerySpecGenerator.java new file mode 100644 index 000000000000..51ae813a2f3c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/FindQuerySpecGenerator.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.generator; + +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; + +/** + * Generate sql find query + */ +public class FindQuerySpecGenerator extends AbstractQueryGenerator implements QuerySpecGenerator { + /** + * Initialization + */ + public FindQuerySpecGenerator() { + } + + @Override + public SqlQuerySpec generateCosmos(CosmosQuery query) { + return super.generateCosmosQuery(query, "SELECT * FROM ROOT r"); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/NativeQueryGenerator.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/NativeQueryGenerator.java new file mode 100644 index 000000000000..3a0f20bde217 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/NativeQueryGenerator.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.generator; + +import com.azure.cosmos.models.SqlQuerySpec; +import org.springframework.data.domain.Sort; +import org.springframework.util.Assert; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Augment custom queries sourced from @Query annotations + */ +public class NativeQueryGenerator { + + private static final NativeQueryGenerator INSTANCE = new NativeQueryGenerator(); + + /** + * @return The native query generator instance + */ + public static NativeQueryGenerator getInstance() { + return INSTANCE; + } + + private SqlQuerySpec cloneWithQueryText(SqlQuerySpec querySpec, String queryText) { + SqlQuerySpec clone = new SqlQuerySpec(); + clone.setQueryText(queryText); + clone.setParameters(querySpec.getParameters()); + return clone; + } + + /** + * Generate sorted query. + * + * @param querySpec SQL query spec + * @param sort Sort + * @return sorted query + * + * WARNING: This function is only to be used with @Query annotations + */ + public SqlQuerySpec generateSortedQuery(SqlQuerySpec querySpec, Sort sort) { + if (sort == null || sort.isUnsorted()) { + return querySpec; + } else { + Matcher matcher = Pattern.compile("\\s(?i)from\\s").matcher(querySpec.getQueryText()); + matcher.find(); + int beginIndex = matcher.start(0) + 6; + String tableName = querySpec.getQueryText().substring(beginIndex); + tableName = tableName.substring(0, tableName.indexOf(" ")); + + String querySort = AbstractQueryGenerator.generateQuerySort(sort, tableName); + String queryText = querySpec.getQueryText() + " " + querySort; + return cloneWithQueryText(querySpec, queryText); + } + } + + /** + * Generate count query. + * + * @param querySpec SQL query spec. + * @return count query + * + * WARNING: This function is only to be used with @Query annotations + */ + public SqlQuerySpec generateCountQuery(SqlQuerySpec querySpec) { + String queryText = querySpec.getQueryText(); + int fromIndex = queryText.toLowerCase(Locale.US).indexOf(" from "); + Assert.isTrue(fromIndex >= 0, "query missing from keyword, query=" + queryText); + + String countQueryText = "select value count(1) " + queryText.substring(fromIndex); + return cloneWithQueryText(querySpec, countQueryText); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/QuerySpecGenerator.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/QuerySpecGenerator.java new file mode 100644 index 000000000000..03cec6d6e2e8 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/QuerySpecGenerator.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.generator; + +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; + +/** + * 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(CosmosQuery query); +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/generator/package-info.java new file mode 100644 index 000000000000..4992691e70db --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.core.generator; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentEntity.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentEntity.java new file mode 100644 index 000000000000..b550e9a6239c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentEntity.java @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentProperty.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentProperty.java new file mode 100644 index 000000000000..092bf4e3b2d8 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentProperty.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.mapping; + +import com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CompositeIndex.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CompositeIndex.java new file mode 100644 index 000000000000..a188d7a49aff --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CompositeIndex.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.mapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for specifying a composite index on CosmosIndexPolicy + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface CompositeIndex { + + /** + * Array of composite index paths + * @return String + */ + CompositeIndexPath[] paths() default {}; +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CompositeIndexPath.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CompositeIndexPath.java new file mode 100644 index 000000000000..ab3d66e14d68 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CompositeIndexPath.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.mapping; + +import com.azure.cosmos.models.CompositePathSortOrder; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for specifying a composite index path + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface CompositeIndexPath { + + /** + * Index path + * @return String + */ + String path() default ""; + + /** + * Index order + * @return CompositePathSortOrder + */ + CompositePathSortOrder order() default CompositePathSortOrder.ASCENDING; + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/Container.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/Container.java new file mode 100644 index 000000000000..fd1187d1dcc8 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/Container.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping; + +import com.azure.spring.data.cosmos.Constants; +import org.springframework.data.annotation.Persistent; + +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; + +/** + * Annotation for Cosmos Container + */ +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Container { + /** + * To set container name + * @return String + */ + String containerName() default Constants.DEFAULT_CONTAINER_NAME; + + /** + * To set request unit + * @return default as "" + */ + String ru() default ""; + + /** + * To set the ttl of container level + * @return default as no ttl + */ + int timeToLive() default Constants.DEFAULT_TIME_TO_LIVE; + + /** + * To set if create container automatically + * @return default as true + */ + boolean autoCreateContainer() default Constants.DEFAULT_AUTO_CREATE_CONTAINER; + + /** + * To set the partition key definition for container + * Should only be used for nested partition key scenario. + * + * For general partition key support, use the {@link PartitionKey} annotation + * By default {@link PartitionKey} annotation will take precedence, unless not specified. + * + * @return partition key path + */ + String partitionKeyPath() default ""; + + /** + * To enable auto scale for container RU limit + * @return default as false + */ + boolean autoScale() default Constants.DEFAULT_AUTO_SCALE; +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosAuditingRegistrar.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosAuditingRegistrar.java new file mode 100644 index 000000000000..e79a9fc05025 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosAuditingRegistrar.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.mapping; + +import com.azure.spring.data.cosmos.Constants; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; +import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; +import org.springframework.data.auditing.config.AuditingConfiguration; +import org.springframework.util.Assert; + +import java.lang.annotation.Annotation; + +/** + * Adapted from MongoAuditingRegistrar.java + */ +class CosmosAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation() + */ + @Override + protected Class getAnnotation() { + return EnableCosmosAuditing.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName() + */ + @Override + protected String getAuditingHandlerBeanName() { + return Constants.AUDITING_HANDLER_BEAN_NAME; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport# + * getAuditHandlerBeanDefinitionBuilder(org.springframework.data.auditing.config.AuditingConfiguration) + */ + @Override + protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) { + Assert.notNull(configuration, "AuditingConfiguration must not be null!"); + + final BeanDefinitionBuilder builder = + BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class); + + final BeanDefinitionBuilder definition = + BeanDefinitionBuilder.genericBeanDefinition(CosmosMappingContext.class); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + + builder.addConstructorArgValue(definition.getBeanDefinition()); + return configureDefaultAuditHandlerAttributes(configuration, builder); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport# + * registerAuditListener(org.springframework.beans.factory.config.BeanDefinition, + * org.springframework.beans.factory.support.BeanDefinitionRegistry) + */ + @Override + protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition, + BeanDefinitionRegistry registry) { + // TODO: consider moving to event listener for auditing rather than injecting the + // IsNewAwareAuditingHandler directly - this would require integrating CosmosTemplate with + // the spring eventing system which would be a chunk of work beyond the scope of this PR + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosIndexingPolicy.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosIndexingPolicy.java new file mode 100644 index 000000000000..c7ab9b72c127 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosIndexingPolicy.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping; + +import com.azure.cosmos.models.IndexingMode; +import com.azure.spring.data.cosmos.Constants; +import org.springframework.data.annotation.Persistent; + +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; + +/** + * Annotation for cosmos indexing policy. + * Using this annotation will NOT overwrite the cosmos indexing policy currently in Azure Portal by default. See `overwritePolicy()` for more details. + * To prevent overwriting the Indexing Policy defined on Azure Portal, Indexing Policy defined on the SDK through this annotation should be identical or set overwritePolicy flag to false. + */ +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CosmosIndexingPolicy { + /** + * Whether or not to overwrite the indexing policy specified in the Portal. + * @return default as false + */ + boolean overwritePolicy() default Constants.DEFAULT_INDEXING_POLICY_OVERWRITE_POLICY; + + /** + * To set automatic indexing + * @return default as true + */ + boolean automatic() default Constants.DEFAULT_INDEXING_POLICY_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 {}; + + + /** + * Composite Indexes + * @return CompositeIndexDefinition[] + */ + CompositeIndex[] compositeIndexes() default {}; +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosMappingContext.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosMappingContext.java new file mode 100644 index 000000000000..37ff147eaff1 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosMappingContext.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.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; + + /** + * Construct a {@link CosmosMappingContext} instance, with the {@link CosmosSimpleTypes} as simple type holder. + */ + public CosmosMappingContext() { + setSimpleTypeHolder(CosmosSimpleTypes.HOLDER); + } + + @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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosPersistentEntity.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosPersistentEntity.java new file mode 100644 index 000000000000..570a12ed79ee --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosPersistentEntity.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.mapping; + +import org.springframework.data.mapping.PersistentEntity; + +/** + * Represents a cosmos persistent entity. + */ +public interface CosmosPersistentEntity extends PersistentEntity { + + /** + * To get container of entity + * @return String + */ + String getContainer(); + + /** + * To get language + * @return String + */ + String getLanguage(); +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosPersistentProperty.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosPersistentProperty.java new file mode 100644 index 000000000000..780b8cbd8137 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosPersistentProperty.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.mapping; + +import org.springframework.data.mapping.PersistentProperty; + +/** + * Interface for cosmos persistent property + */ +public interface CosmosPersistentProperty extends PersistentProperty { +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosSimpleTypes.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosSimpleTypes.java new file mode 100644 index 000000000000..239669d2436a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosSimpleTypes.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.mapping; + +import org.springframework.data.mapping.model.SimpleTypeHolder; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Simple constant holder for a {@link SimpleTypeHolder} enriched with Cosmos specific simple types. + */ +final class CosmosSimpleTypes { + + private static final Set> COSMOS_SIMPLE_TYPES; + + static { + Set> simpleTypes = new HashSet<>(); + simpleTypes.add(UUID.class); + + COSMOS_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); + } + + /** + * The Cosmos {@link SimpleTypeHolder}. + */ + public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(COSMOS_SIMPLE_TYPES, true) { + + @Override + public boolean isSimpleType(Class type) { + + if (type.isEnum()) { + return true; + } + + return super.isSimpleType(type); + } + }; + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosUniqueKey.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosUniqueKey.java new file mode 100644 index 000000000000..044940062463 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosUniqueKey.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping; + +import org.springframework.data.annotation.Persistent; + +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; + +/** + * Represents a unique key on that enforces uniqueness constraint on items in the container in the Azure Cosmos DB + * service. + *

+ * 1) For containers, the value of partition key is implicitly a part of each unique key. + *

+ * 2) Uniqueness constraint is also enforced for missing values. + *

+ * For instance, if unique key policy defines a unique key with single property path, there could be only one item that + * has missing value for this property. + * + * @see CosmosUniqueKeyPolicy + */ +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CosmosUniqueKey { + + /** + * A set of paths which must be unique for each item in the Azure Cosmos DB service. + *

+ * The paths to enforce uniqueness on. Each path is a rooted path of the unique property in the item, such as + * "/name/first". + * + * @return unique paths + */ + String[] paths() default {}; +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosUniqueKeyPolicy.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosUniqueKeyPolicy.java new file mode 100644 index 000000000000..6009141b562f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/CosmosUniqueKeyPolicy.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping; + +import org.springframework.data.annotation.Persistent; + +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; + +/** + * Represents the unique key policy configuration for specifying uniqueness constraints on items in the container in the + * Azure Cosmos DB service. + */ +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CosmosUniqueKeyPolicy { + + /** + * Set of unique keys which enforce uniqueness constraint on items in the container + * in the Azure Cosmos DB service. + * + * @return unique keys + */ + CosmosUniqueKey[] uniqueKeys() default {}; +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/EnableCosmosAuditing.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/EnableCosmosAuditing.java new file mode 100644 index 000000000000..6302f6985b1b --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/EnableCosmosAuditing.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.mapping; + +import org.springframework.context.annotation.Import; +import org.springframework.data.auditing.DateTimeProvider; +import org.springframework.data.domain.AuditorAware; + +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; + +/** + * Adapted from spring-data-mongodb + * @see EnableMongoAuditing.java + */ +@Inherited +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(CosmosAuditingRegistrar.class) +public @interface EnableCosmosAuditing { + + /** + * Configures the {@link AuditorAware} bean to be used to lookup the current principal. + * + * @return default empty string + */ + String auditorAwareRef() default ""; + + /** + * Configures whether the creation and modification dates are set. Defaults to {@literal true}. + * + * @return default true + */ + boolean setDates() default true; + + /** + * Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}. + * + * @return default true + */ + boolean modifyOnCreate() default true; + + /** + * Configures a {@link DateTimeProvider} bean name that allows customizing the DateTime to be + * used for setting creation and modification dates. + * + * @return default empty string + */ + String dateTimeProviderRef() default ""; +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/GeneratedValue.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/GeneratedValue.java new file mode 100644 index 000000000000..156e5078b6f8 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/GeneratedValue.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Used to specify that an entities id field should generate a value + */ +@Target(FIELD) +@Retention(RUNTIME) +public @interface GeneratedValue { +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/PartitionKey.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/PartitionKey.java new file mode 100644 index 000000000000..937d70201aa5 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/PartitionKey.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping; + +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 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/AbstractCosmosEventListener.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/AbstractCosmosEventListener.java new file mode 100644 index 000000000000..004b1ea0a56e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/AbstractCosmosEventListener.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.core.GenericTypeResolver; + +/** + * Abstract Event Listener. Can be extended and registered as a bean to capture spring application events + * + * Ported to cosmos from spring-data-mongo + * https://github.com/spring-projects/spring-data-mongodb/blob/main/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractMongoEventListener.java + * + */ +public abstract class AbstractCosmosEventListener implements ApplicationListener> { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractCosmosEventListener.class); + private final Class domainClass; + + /** + * Creates a new {@link AbstractCosmosEventListener}. + */ + public AbstractCosmosEventListener() { + Class typeArgument = GenericTypeResolver.resolveTypeArgument(this.getClass(), AbstractCosmosEventListener.class); + this.domainClass = typeArgument == null ? Object.class : typeArgument; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void onApplicationEvent(CosmosMappingEvent event) { + if (event instanceof AfterLoadEvent) { + AfterLoadEvent afterLoadEvent = (AfterLoadEvent) event; + + if (domainClass.isAssignableFrom(afterLoadEvent.getType())) { + onAfterLoad((AfterLoadEvent) event); + } + } + } + + /** + * Captures {@link AfterLoadEvent}. + * + * @param event will never be {@literal null}. + */ + public void onAfterLoad(AfterLoadEvent event) { + if (LOG.isDebugEnabled()) { + LOG.debug("onAfterLoad({})", event.getDocument()); + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/AfterLoadEvent.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/AfterLoadEvent.java new file mode 100644 index 000000000000..7691674fdb2d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/AfterLoadEvent.java @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.util.Assert; + +/** + * Spring Application Event that fires for all reads after a document is loaded and before it is serialized to + * a domain object + * + * Ported to cosmos from spring-data-mongo + * https://github.com/spring-projects/spring-data-mongodb/blob/main/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterLoadEvent.java + */ +public class AfterLoadEvent extends CosmosMappingEvent { + + private static final long serialVersionUID = 1L; + + /** + * Type + */ + private final Class type; + + /** + * Creates a new {@link AfterLoadEvent} for the given {@link JsonNode}, type and collectionName. + * + * @param document must not be {@literal null}. + * @param type must not be {@literal null}. + * @param containerName must not be {@literal null}. + */ + public AfterLoadEvent(JsonNode document, Class type, String containerName) { + super(document, document, containerName); + Assert.notNull(type, "Type must not be null!"); + this.type = type; + } + + /** + * Returns the type for which the {@link AfterLoadEvent} shall be invoked for. + * + * @return never {@literal null}. + */ + public Class getType() { + return type; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/CosmosMappingEvent.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/CosmosMappingEvent.java new file mode 100644 index 000000000000..4924cdd0b182 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/CosmosMappingEvent.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.context.ApplicationEvent; +import org.springframework.lang.Nullable; + +/** + * Base Class that all cosmos related spring application events extend + * + * Ported to cosmos from spring-data-mongo + * https://github.com/spring-projects/spring-data-mongodb/blob/main/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java + */ +public class CosmosMappingEvent extends ApplicationEvent { + + private static final long serialVersionUID = 1L; + + /** + * Document + */ + private final @Nullable JsonNode document; + + /** + * Container name + */ + private final @Nullable String containerName; + + /** + * Creates new {@link CosmosMappingEvent}. + * + * @param source must not be {@literal null}. + * @param document can be {@literal null}. + * @param containerName can be {@literal null}. + */ + public CosmosMappingEvent(T source, @Nullable JsonNode document, @Nullable String containerName) { + + super(source); + this.document = document; + this.containerName = containerName; + } + + /** + * @return {@literal null} if not set. + */ + public @Nullable JsonNode getDocument() { + return document; + } + + /** + * Get the container the event refers to. + * + * @return {@literal null} if not set. + */ + public @Nullable String getContainerName() { + return containerName; + } + + /* + * (non-Javadoc) + * @see java.util.EventObject#getSource() + */ + @SuppressWarnings({ "unchecked" }) + @Override + public T getSource() { + return (T) super.getSource(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/package-info.java new file mode 100644 index 000000000000..c1ee83dfbc64 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/event/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.azure.spring.data.cosmos.core.mapping.event; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/mapping/package-info.java new file mode 100644 index 000000000000..673ed782e383 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.core.mapping; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/package-info.java new file mode 100644 index 000000000000..e198fccd1791 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.core; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosPageImpl.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosPageImpl.java new file mode 100644 index 000000000000..73e1e7873c92 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosPageImpl.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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 + /** + * Offset + */ + 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosPageRequest.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosPageRequest.java new file mode 100644 index 000000000000..be8c3d2c66d8 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosPageRequest.java @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.query; + +import com.azure.cosmos.models.FeedResponse; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +import java.util.Objects; + +/** + * CosmosPageRequest representing page request during pagination query, field + * {@link FeedResponse#getContinuationToken()} 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; + + /** + * Offset + */ + private long offset; + + // Request continuation token used to resume query + /** + * Request continuation token + */ + 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 the continuation token of the request. + */ + 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 the continuation token of the request. + */ + 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 PageRequest 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 = Objects.equals(requestContinuation, that.requestContinuation); + + return continuationTokenEquals + && super.equals(that); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosQuery.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosQuery.java new file mode 100644 index 000000000000..b1b19794b6a6 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosQuery.java @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.query; + +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * Class for cosmos query + */ +public class CosmosQuery { + + private final Criteria criteria; + + private Sort sort = Sort.unsorted(); + + private Pageable pageable = Pageable.unpaged(); + + private int limit; + + private long offset; + + /** + * Initialization + * + * @param criteria object + */ + public CosmosQuery(@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; + } + + /** + * To get limit number + * + * @return int limit + */ + public int getLimit() { + return limit; + } + + /** + * To get offset number + * + * @return long offset + */ + public long getOffset() { + return offset; + } + + /** + * To set limit number + * + * @param limit int + * @deprecated use withLimit instead + */ + @Deprecated + public void setLimit(int limit) { + if (this.limit == 0) { + this.limit = limit; + } + } + + /** + * To set limit number + * + * @param limit int + * @return CosmosQuery object + */ + public CosmosQuery withLimit(int limit) { + if (this.limit == 0) { + this.limit = limit; + } + return this; + } + + /** + * To set limit number and offset number + * + * @param offset long + * @param limit int + * @return CosmosQuery object + */ + public CosmosQuery withOffsetAndLimit(long offset, int limit) { + if (this.limit == 0) { + this.limit = limit; + } + if (this.offset == 0) { + this.offset = offset; + } + return this; + } + + /** + * With Sort + * + * @param sort Sort + * @return CosmosQuery object + */ + public CosmosQuery with(@NonNull Sort sort) { + if (sort.isSorted()) { + this.sort = sort.and(this.sort); + } + + return this; + } + + /** + * With Sort + * + * @param pageable Sort + * @return CosmosQuery object + */ + public CosmosQuery 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 -> { + // If there is equal criteria, then it is a single partition query + if (isEqualCriteria(criteria1)) { + return false; + } + // IN is a special case, where we want to first check if the partition key is used with IN clause + if (criteria1.getType() == CriteriaType.IN && criteria1.getSubjectValues().size() == 1) { + @SuppressWarnings("unchecked") + Collection collection = (Collection) criteria1.getSubjectValues().get(0); + // IN query types can have multiple values, + // so we are checking the internal collection of the criteria + return collection.size() != 1; + } + return !hasKeywordAnd(); + }).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; + } + + private boolean hasKeywordAnd() { + // If there is AND keyword in DocumentQuery, the top node of Criteria must be AND type. + return this.criteria.getType() == CriteriaType.AND; + } + + private boolean isEqualCriteria(Criteria criteria) { + return criteria.getType() == CriteriaType.IS_EQUAL; + } + + /** + * 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()); + } + + /** + * Returns true if this criteria or sub-criteria has partition key field present as one of the subjects. + * @param partitionKeyFieldName partition key field name + * @return returns true if this criteria or sub criteria has partition key field present as one of the subjects. + */ + public boolean hasPartitionKeyCriteria(@NonNull String partitionKeyFieldName) { + if (partitionKeyFieldName.isEmpty()) { + return false; + } + + final Optional criteria = this.getSubjectCriteria(this.criteria, partitionKeyFieldName); + return criteria.isPresent(); + } + + /** + * Returns partition key value based on the criteria. + * @param domainType domain type + * @param entity class type + * @return Optional of partition key value + */ + public Optional getPartitionKeyValue(@NonNull Class domainType) { + CosmosEntityInformation instance = CosmosEntityInformation.getInstance(domainType); + String partitionKeyFieldName = instance.getPartitionKeyFieldName(); + if (partitionKeyFieldName == null + || partitionKeyFieldName.isEmpty() + || isCrossPartitionQuery(Collections.singletonList(partitionKeyFieldName))) { + return Optional.empty(); + } + + final Optional criteria = this.getSubjectCriteria(this.criteria, partitionKeyFieldName); + return criteria.map(criteria1 -> { + // If the criteria has ignoreCase, then we cannot set the partition key + // because of case sensitivity of partition key + if (!criteria1.getIgnoreCase().equals(Part.IgnoreCaseType.NEVER)) { + return null; + } + if (criteria1.getType() == CriteriaType.IN && criteria1.getSubjectValues().size() == 1) { + @SuppressWarnings("unchecked") + Collection collection = (Collection) criteria1.getSubjectValues().get(0); + // IN query types can have multiple values, + // so we are checking the internal collection of the criteria + if (collection.size() == 1) { + return collection.iterator().next(); + } else { + return null; + } + } + return criteria1.getSubjectValues().get(0); + }); + } + + /** + * 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosSliceImpl.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosSliceImpl.java new file mode 100644 index 000000000000..8e610187b8e9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CosmosSliceImpl.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.query; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.SliceImpl; + +import java.util.List; +import java.util.Objects; + +/** + * {@code CosmosSliceImpl} implementation. + * + * @param the type of which the CosmosSliceImpl consists. + */ +public class CosmosSliceImpl extends SliceImpl { + + private static final long serialVersionUID = 4487077496284030775L; + + // 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 + /** + * Offset + */ + private final long offset; + + /** + * Constructor of {@code CosmosSliceImpl}. + * + * @param content the content of this page, must not be {@literal null}. + * @param pageable the paging information, must not be {@literal null}. + * @param hasNext whether the query has any more results to fetch + */ + public CosmosSliceImpl(List content, Pageable pageable, boolean hasNext) { + super(content, pageable, hasNext); + this.offset = pageable.getOffset(); + } + + @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 CosmosSliceImpl that = (CosmosSliceImpl) o; + return offset == that.offset; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), offset); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/Criteria.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/Criteria.java new file mode 100644 index 000000000000..d95ad67a4130 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/Criteria.java @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.query; + +import org.springframework.data.repository.query.parser.Part; +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; + private Part.IgnoreCaseType ignoreCase; + + /** + * Ignore case flag + * @return ignore case flag + */ + public Part.IgnoreCaseType getIgnoreCase() { + return ignoreCase; + } + + /** + * 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 and ignore case + * @param type CriteriaType + * @param subject subject + * @param values subject value + * @param ignoreCase ignore case flag + * @return Criteria instance + */ + public static Criteria getInstance(CriteriaType type, @NonNull String subject, + @NonNull List values, @NonNull Part.IgnoreCaseType ignoreCase) { + final Criteria criteria = new Criteria(type); + + criteria.subject = subject; + criteria.subjectValues = values; + criteria.ignoreCase = ignoreCase; + 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CriteriaType.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CriteriaType.java new file mode 100644 index 000000000000..50e2354b9171 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/CriteriaType.java @@ -0,0 +1,328 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.query; + +import org.springframework.data.repository.query.parser.Part; +import org.springframework.lang.NonNull; + +import java.beans.ConstructorProperties; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Enum of criteria type + */ +public enum CriteriaType { + + /** + * All + */ + ALL(""), + + /** + * Is equal + */ + IS_EQUAL("="), + + /** + * Or + */ + OR("OR"), + + /** + * And + */ + AND("AND"), + + /** + * Not + */ + NOT("<>"), + + /** + * Before + */ + BEFORE("<"), + + /** + * After + */ + AFTER(">"), + + /** + * In + */ + IN("IN"), + + /** + * Not in + */ + NOT_IN("NOT IN"), + + /** + * Is null + */ + IS_NULL("IS_NULL"), + + /** + * Is not null + */ + IS_NOT_NULL("NOT IS_NULL"), + + /** + * Less than + */ + LESS_THAN("<"), + + /** + * Less than or equal + */ + LESS_THAN_EQUAL("<="), + + /** + * Greater than + */ + GREATER_THAN(">"), + + /** + * Greater than or equal + */ + GREATER_THAN_EQUAL(">="), + + /** + * Contain + */ + CONTAINING("CONTAINS"), + + /** + * Ends with + */ + ENDS_WITH("ENDSWITH"), + + /** + * Starts with + */ + STARTS_WITH("STARTSWITH"), + + /** + * True + */ + TRUE("= true"), + + /** + * False + */ + FALSE("= false"), + + /** + * Between + */ + BETWEEN("BETWEEN"), + + /** + * Array contains + */ + ARRAY_CONTAINS("ARRAY_CONTAINS"), + + /** + * String equals + */ + STRING_EQUALS("STRINGEQUALS"), + + /** + * Not Contain + */ + NOT_CONTAINING("NOT CONTAINS"); + + 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.NOT_CONTAINING, CriteriaType.NOT_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 NOT_CONTAINING: + case ENDS_WITH: + case STARTS_WITH: + case ARRAY_CONTAINS: + case STRING_EQUALS: + 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 NOT_CONTAINING: + case ENDS_WITH: + case STARTS_WITH: + case IS_NULL: + case IS_NOT_NULL: + case ARRAY_CONTAINS: + case STRING_EQUALS: + return true; + default: + return false; + } + } + + /** + * Check if CriteriaType operation is a function. + * + * @param type CriteriaType + * @return True if match, or false. + */ + public static boolean isFunctionWithCaseSensitiveSupport(CriteriaType type) { + switch (type) { + case CONTAINING: + case NOT_CONTAINING: + case ENDS_WITH: + case STARTS_WITH: + case STRING_EQUALS: + 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/core/query/package-info.java new file mode 100644 index 000000000000..c1c80354c766 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.core.query; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/ConfigurationException.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/ConfigurationException.java new file mode 100644 index 000000000000..f6bd7e1bf6c7 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/ConfigurationException.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/CosmosAccessException.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/CosmosAccessException.java new file mode 100644 index 000000000000..2f1869dac6d1 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/CosmosAccessException.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.exception; + +import com.azure.cosmos.CosmosException; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Public class extending DataAccessException, exposes innerException. + * Every API in {@link CosmosRepository} + * and {@link ReactiveCosmosRepository} + * should throw {@link CosmosAccessException}. + * 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 CosmosAccessException extends DataAccessException { + + /** + * Cosmos exception. + */ + protected final CosmosException cosmosException; + + /** + * Construct a {@code CosmosDBAccessException} with the specified detail message. + * @param msg the detail message + */ + public CosmosAccessException(String msg) { + super(msg); + this.cosmosException = null; + } + + /** + * Construct a {@code CosmosDBAccessException} with the specified detail message. + * and nested exception. + * @param msg the detail message + * @param cause the nested Throwable + */ + public CosmosAccessException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + if (cause instanceof CosmosException) { + this.cosmosException = (CosmosException) cause; + } else { + this.cosmosException = null; + } + } + + /** + * Construct a {@code CosmosDBAccessException} with the specified detail message + * and nested exception. + * + * @param msg the detail message + * @param cause the nested exception + */ + public CosmosAccessException(@Nullable String msg, @Nullable Exception cause) { + super(msg, cause); + this.cosmosException = cause instanceof CosmosException + ? (CosmosException) cause + : null; + } + + /** + * To get exception object for cosmos client + * @return CosmosException + */ + public CosmosException getCosmosException() { + return cosmosException; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/CosmosExceptionUtils.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/CosmosExceptionUtils.java new file mode 100644 index 000000000000..eb903d1217db --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/CosmosExceptionUtils.java @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.exception; + +import com.azure.cosmos.CosmosException; +import com.azure.spring.data.cosmos.common.CosmosUtils; +import com.azure.spring.data.cosmos.core.ResponseDiagnosticsProcessor; +import org.springframework.util.ObjectUtils; +import reactor.core.Exceptions; +import reactor.core.publisher.Mono; + +/** + * To handle and throw a cosmos db exception when access the database + */ +public class CosmosExceptionUtils { + + /** + * To throw a CosmosDBAccessException + * + * @param message the detail message + * @param throwable exception + * @param responseDiagnosticsProcessor response diagnostics processor + * @param type class of Mono + * @return Mono instance + * @throws CosmosAccessException for operations on cosmos db + */ + public static Mono exceptionHandler(String message, Throwable throwable, + ResponseDiagnosticsProcessor responseDiagnosticsProcessor) { + if (ObjectUtils.isEmpty(message)) { + message = "Failed to access cosmos db database"; + } + // Unwrap the exception in case if it is a reactive exception + final Throwable unwrappedThrowable = Exceptions.unwrap(throwable); + if (unwrappedThrowable instanceof CosmosException) { + CosmosException cosmosException = (CosmosException) unwrappedThrowable; + CosmosUtils.fillAndProcessCosmosExceptionDiagnostics(responseDiagnosticsProcessor, cosmosException); + } + throw new CosmosAccessException(message, unwrappedThrowable); + } + + /** + * To find an exceptionHandler for a exception and return empty Mono if the exception status code is not found + * + * @param message the detail message + * @param throwable exception + * @param responseDiagnosticsProcessor Response Diagnostics Processor + * @param type class of Mono + * @return Mono instance + * @throws CosmosAccessException for operations on cosmos db + */ + public static Mono findAPIExceptionHandler(String message, Throwable throwable, + ResponseDiagnosticsProcessor responseDiagnosticsProcessor) { + if (ObjectUtils.isEmpty(message)) { + message = "Failed to find item"; + } + // Unwrap the exception in case if it is a reactive exception + final Throwable unwrappedThrowable = Exceptions.unwrap(throwable); + if (unwrappedThrowable instanceof CosmosException) { + final CosmosException cosmosClientException = (CosmosException) unwrappedThrowable; + CosmosUtils.fillAndProcessCosmosExceptionDiagnostics(responseDiagnosticsProcessor, cosmosClientException); + if (cosmosClientException.getStatusCode() == 404) { + return Mono.empty(); + } + } + throw new CosmosAccessException(message, unwrappedThrowable); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/DatabaseCreationException.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/DatabaseCreationException.java new file mode 100644 index 000000000000..6b42de838600 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/DatabaseCreationException.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.exception; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * General exception for illegal creation of cosmos db + */ +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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/IllegalCollectionException.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/IllegalCollectionException.java new file mode 100644 index 000000000000..991338998643 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/IllegalCollectionException.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.exception; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * General exception for illegal collection of cosmos db + */ +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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/IllegalQueryException.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/IllegalQueryException.java new file mode 100644 index 000000000000..e1027f879cc3 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/IllegalQueryException.java @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.exception; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * General exception for illegal query of cosmos db + */ +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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/exception/package-info.java new file mode 100644 index 000000000000..3094644570a7 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.exception; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/package-info.java new file mode 100644 index 000000000000..30260f8b7c4e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Spring Cloud Azure's Service Bus JMS extension. + */ +package com.azure.spring.jms; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/CosmosRepository.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/CosmosRepository.java new file mode 100644 index 000000000000..5763e9695313 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/CosmosRepository.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository; + +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.PartitionKey; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.PagingAndSortingRepository; + +import java.io.Serializable; +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); + + /** + * Patches an entity by its id and partition key with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param type class of domain type + * @return the patched entity + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + S save(ID id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations); + + /** + * Patches an entity by its id and partition key with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched entity + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + S save(ID id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options); + + /** + * Returns list of items in a specific partition + * + * @param partitionKey partition key value + * @return Iterable of items with partition key value + */ + Iterable findAll(PartitionKey partitionKey); + +} + diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/Query.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/Query.java new file mode 100644 index 000000000000..fb181ae43140 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/Query.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; +import org.springframework.data.annotation.QueryAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to declare finder queries directly on repository methods. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@QueryAnnotation +public @interface Query { + /** + * value of the query + * + * @return the value + */ + String value() default ""; +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/ReactiveCosmosRepository.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/ReactiveCosmosRepository.java new file mode 100644 index 000000000000..0499e2e9c679 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/ReactiveCosmosRepository.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.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); + + /** + * Patches an entity by its id and partition key with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param type class of domain type + * @return the patched entity + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + Mono save(K id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations); + + /** + * Patches an entity by its id and partition key with CosmosPatchItemRequestOptions + * + * @param id must not be {@literal null} + * @param partitionKey must not be {@literal null} + * @param domainType must not be {@literal null} + * @param patchOperations must not be {@literal null}, max operations is 10 + * @param options Optional CosmosPatchItemRequestOptions, e.g. options.setFilterPredicate("FROM products p WHERE p.used = false"); + * @param type class of domain type + * @return the patched entity + * @throws IllegalArgumentException in case the given {@code id} is {@literal null}. + */ + Mono save(K id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options); + + /** + * 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoriesRegistrar.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoriesRegistrar.java new file mode 100644 index 000000000000..9975dfbf92b2 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoriesRegistrar.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoryConfigurationExtension.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoryConfigurationExtension.java new file mode 100644 index 000000000000..732dea8fcdbf --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoryConfigurationExtension.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.config; + +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import com.azure.spring.data.cosmos.repository.support.CosmosRepositoryFactoryBean; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +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.COSMOS_MODULE_NAME; + } + + @Override + public String getModulePrefix() { + return Constants.COSMOS_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); + } + + @Override + public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource source) { + final AnnotationAttributes attributes = source.getAttributes(); + builder.addPropertyReference("cosmosOperations", attributes.getString("cosmosTemplateRef")); + } + + // 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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/EnableCosmosRepositories.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/EnableCosmosRepositories.java new file mode 100644 index 000000000000..c399f5c1fef9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/EnableCosmosRepositories.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.config; + +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.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 { + + /** + * To set 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; + + /** + * Configures the name of the {@link CosmosTemplate} bean to be used with the repositories detected. + * + * @return {@literal cosmosTemplate} by default. + */ + String cosmosTemplateRef() default "cosmosTemplate"; +} + diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/EnableReactiveCosmosRepositories.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/EnableReactiveCosmosRepositories.java new file mode 100644 index 000000000000..7ab04879b6e5 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/EnableReactiveCosmosRepositories.java @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.config; + +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.core.ReactiveCosmosTemplate; +import com.azure.spring.data.cosmos.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 { + + /** + * To set 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; + + /** + * Configures the name of the {@link ReactiveCosmosTemplate} bean to be used with the repositories detected. + * + * @return {@literal reactiveCosmosTemplate} by default. + */ + String reactiveCosmosTemplateRef() default "reactiveCosmosTemplate"; +} + diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoriesRegistrar.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoriesRegistrar.java new file mode 100644 index 000000000000..60b59a81726a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoriesRegistrar.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoryConfigurationExtension.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoryConfigurationExtension.java new file mode 100644 index 000000000000..dfc7327fdcaf --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoryConfigurationExtension.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.config; + +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import com.azure.spring.data.cosmos.repository.support.ReactiveCosmosRepositoryFactoryBean; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +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.COSMOS_MODULE_NAME; + } + + @Override + public String getModulePrefix() { + return Constants.COSMOS_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); + } + + @Override + public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource source) { + final AnnotationAttributes attributes = source.getAttributes(); + builder.addPropertyReference("reactiveCosmosOperations", attributes.getString("reactiveCosmosTemplateRef")); + } + + // Overriding this to provide reactive repository support. + @Override + protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { + return metadata.isReactiveRepository(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/config/package-info.java new file mode 100644 index 000000000000..1a285bce2557 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.repository.config; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/package-info.java new file mode 100644 index 000000000000..18d6925d30f9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.repository; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQuery.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQuery.java new file mode 100644 index 000000000000..573da2e87276 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQuery.java @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; + +/** + * Abstract class for cosmos query. + */ +public abstract class AbstractCosmosQuery implements RepositoryQuery { + + private final CosmosQueryMethod method; + + /** + * CosmosOperations + */ + protected 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}. + */ + @Override + public Object execute(Object[] parameters) { + final CosmosParameterAccessor accessor = new CosmosParameterParameterAccessor(method, parameters); + final CosmosQuery query = createQuery(accessor); + + final ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); + final String container = ((CosmosEntityMetadata) method.getEntityInformation()).getContainerName(); + + final CosmosQueryExecution execution = getExecution(accessor, processor.getReturnedType()); + + return execution.execute(query, processor.getReturnedType().getDomainType(), container); + } + + + /** + * Determines the appropriate execution path for a query + * + * @param returnedType The return type of the method + * @param accessor Object for accessing method parameters + * @return the execution type needed to handle the query + */ + protected CosmosQueryExecution getExecution(CosmosParameterAccessor accessor, ReturnedType returnedType) { + if (isDeleteQuery()) { + return new CosmosQueryExecution.DeleteExecution(operations); + } else if (isPageQuery()) { + return new CosmosQueryExecution.PagedExecution(operations, accessor.getPageable()); + } else if (isSliceQuery()) { + return new CosmosQueryExecution.SliceExecution(operations, accessor.getPageable()); + } else if (isExistsQuery()) { + return new CosmosQueryExecution.ExistsExecution(operations); + } else if (isCountQuery()) { + return new CosmosQueryExecution.CountExecution(operations); + } else if (isCollectionQuery()) { + return new CosmosQueryExecution.MultiEntityExecution(operations); + } else { + return new CosmosQueryExecution.SingleEntityExecution(operations, returnedType); + } + } + + /** + * Get method of query + * + * @return CosmosQueryMethod + */ + public CosmosQueryMethod getQueryMethod() { + return method; + } + + /** + * Creates a query. + * @param accessor Cosmos parameter accessor. + * @return a Cosmos query. + */ + protected abstract CosmosQuery createQuery(CosmosParameterAccessor accessor); + + /** + * @return whether this is a deletion query. + */ + protected abstract boolean isDeleteQuery(); + + /** + * @return whether this is an exists query. + */ + protected abstract boolean isExistsQuery(); + + /** + * @return whether this is a count query. + */ + protected abstract boolean isCountQuery(); + + /** + * @return whether this is a page query. + */ + protected boolean isPageQuery() { + return method.isPageQuery(); + } + + /** + * @return whether this is a collection query. + */ + protected boolean isCollectionQuery() { + return method.isCollectionQuery(); + } + + /** + * @return whether this is a slice query. + */ + protected boolean isSliceQuery() { + return method.isSliceQuery(); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQuery.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQuery.java new file mode 100644 index 000000000000..7cd5d059416e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQuery.java @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import reactor.core.publisher.Mono; + +/** + * Abstract class for reactive cosmos query. + */ +public abstract class AbstractReactiveCosmosQuery implements RepositoryQuery { + + private final ReactiveCosmosQueryMethod method; + + /** + * Reactive Cosmos operations + */ + protected 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}. + */ + @Override + public Object execute(Object[] parameters) { + final ReactiveCosmosParameterAccessor accessor = + new ReactiveCosmosParameterParameterAccessor(method, parameters); + final CosmosQuery query = createQuery(accessor); + + final ResultProcessor processor = + method.getResultProcessor().withDynamicProjection(accessor); + final String containerName = + ((ReactiveCosmosEntityMetadata) method.getEntityInformation()).getContainerName(); + + final ReactiveCosmosQueryExecution execution = getExecution(processor.getReturnedType()); + return execution.execute(query, processor.getReturnedType().getDomainType(), containerName); + } + + /** + * Determines the appropriate execution path for a reactive query + * + * @throws IllegalArgumentException if execution requires paging + * @param returnedType The return type of the method + * @return the execution type needed to handle the query + */ + protected ReactiveCosmosQueryExecution getExecution(ReturnedType returnedType) { + if (isDeleteQuery()) { + return new ReactiveCosmosQueryExecution.DeleteExecution(operations); + } else if (isPageQuery()) { + throw new IllegalArgumentException("Paged Query is not supported by reactive cosmos " + + "db"); + } else if (isExistsQuery()) { + return new ReactiveCosmosQueryExecution.ExistsExecution(operations); + } else if (isCountQuery()) { + return new ReactiveCosmosQueryExecution.CountExecution(operations); + } else if (isReactiveSingleResultQuery()) { + return new ReactiveCosmosQueryExecution.SingleEntityExecution(operations, returnedType); + } else { + return new ReactiveCosmosQueryExecution.MultiEntityExecution(operations); + } + } + + /** + * Get method of query + * + * @return ReactiveCosmosQueryMethod + */ + public ReactiveCosmosQueryMethod getQueryMethod() { + return method; + } + + /** + * Creates a query. + * @param accessor Reactive Cosmos parameter accessor. + * @return a Cosmos query. + */ + protected abstract CosmosQuery createQuery(ReactiveCosmosParameterAccessor accessor); + + /** + * @return whether this is a deletion query. + */ + protected abstract boolean isDeleteQuery(); + + /** + * @return whether this is an exists query. + */ + protected abstract boolean isExistsQuery(); + + /** + * @return whether this is a count query. + */ + protected abstract boolean isCountQuery(); + + /** + * @return whether this is a page query. + */ + protected boolean isPageQuery() { + return method.isPageQuery(); + } + + private boolean isReactiveSingleResultQuery() { + return method.getReactiveWrapper() != null && method.getReactiveWrapper().equals(Mono.class); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosEntityMetadata.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosEntityMetadata.java new file mode 100644 index 000000000000..204c69d55b3a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosEntityMetadata.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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 container name from the given entity + * @return String + */ + String getContainerName(); +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameter.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameter.java new file mode 100644 index 000000000000..90a3dddc2986 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameter.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameterAccessor.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameterAccessor.java new file mode 100644 index 000000000000..9f112e1084e3 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameterAccessor.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameterParameterAccessor.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameterParameterAccessor.java new file mode 100644 index 000000000000..57da9662636c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameterParameterAccessor.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameters.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameters.java new file mode 100644 index 000000000000..4ed1187a7969 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosParameters.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryCreator.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryCreator.java new file mode 100644 index 000000000000..65c3f25f0cb8 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryCreator.java @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.core.mapping.CosmosPersistentProperty; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.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, part.shouldIgnoreCase()); + } + + @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 CosmosQuery complete(@NonNull Criteria criteria, @NonNull Sort sort) { + return new CosmosQuery(criteria).with(sort); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryExecution.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryExecution.java new file mode 100644 index 000000000000..a291fb73b347 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryExecution.java @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.query.ReturnedType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * 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(CosmosQuery query, Class type, String container); + + /** + * Container operation implementation to execute a container name query + */ + final class ContainerExecution implements CosmosQueryExecution { + + private final CosmosOperations operations; + + /** + * Creates a new instance of container execution. + * + * @param operations the Cosmos operations + */ + public ContainerExecution(CosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery query, Class type, String container) { + return operations.getContainerName(type); + } + } + + /** + * Find operation implementation to execute a find query for multiple items + */ + final class MultiEntityExecution implements CosmosQueryExecution { + + private final CosmosOperations operations; + + /** + * Creates a new instance of multi-entity execution. + * + * @param operations the Cosmos operations + */ + public MultiEntityExecution(CosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery query, Class type, String container) { + return operations.find(query, type, container); + } + } + + /** + * Find operation implementation to execute a find query for a single item + */ + final class SingleEntityExecution implements CosmosQueryExecution { + + private final CosmosOperations operations; + private final ReturnedType returnedType; + + /** + * Creates a new instance of single-entity execution. + * + * @param operations the Cosmos operations + * @param returnedType the returned type + */ + public SingleEntityExecution(CosmosOperations operations, ReturnedType returnedType) { + this.operations = operations; + this.returnedType = returnedType; + } + + @Override + public Object execute(CosmosQuery query, Class type, String collection) { + Iterable resultsIterable = operations.find(query, type, collection); + final List results = new ArrayList<>(); + resultsIterable.forEach(results::add); + final Object result; + if (results.isEmpty()) { + result = null; + } else if (results.size() == 1) { + result = results.get(0); + } else { + throw new CosmosAccessException("Too many results - return type " + + returnedType.getReturnedType() + + " is not of type Iterable but find returned " + + results.size() + + " results"); + } + + if (returnedType.getReturnedType() == Optional.class) { + return result == null ? Optional.empty() : Optional.of(result); + } else { + return result; + } + } + } + + /** + * exist operation implementation to execute a exists query + */ + final class ExistsExecution implements CosmosQueryExecution { + + private final CosmosOperations operations; + + /** + * Creates a new instance of exists execution. + * + * @param operations the Cosmos operations + */ + public ExistsExecution(CosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery 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; + + /** + * Creates a new instance of delete execution. + * + * @param operations the Cosmos operations + */ + public DeleteExecution(CosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery 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; + + /** + * Creates a new instance of paged execution. + * + * @param operations the Cosmos operations + * @param pageable the pageable + */ + public PagedExecution(CosmosOperations operations, Pageable pageable) { + this.operations = operations; + this.pageable = pageable; + } + + @Override + public Object execute(CosmosQuery 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); + } + } + + /** + * sliceQuery operation implementation to execute a sliceQuery query + */ + final class SliceExecution implements CosmosQueryExecution { + private final CosmosOperations operations; + private final Pageable pageable; + + /** + * Creates a new instance of slice execution. + * + * @param operations the Cosmos operations + * @param pageable the pageable + */ + public SliceExecution(CosmosOperations operations, Pageable pageable) { + this.operations = operations; + this.pageable = pageable; + } + + @Override + public Object execute(CosmosQuery 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.sliceQuery(query, type, container); + } + } + + /** + * count operation implementation to execute a count query + */ + final class CountExecution implements CosmosQueryExecution { + + private final CosmosOperations operations; + + /** + * Creates a new instance of count execution. + * + * @param operations the Cosmos operations + */ + public CountExecution(CosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery query, Class type, String container) { + return operations.count(query, container); + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryMethod.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryMethod.java new file mode 100644 index 000000000000..2e1ed6ab3193 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryMethod.java @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.repository.Query; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.springframework.core.annotation.AnnotatedElementUtils; +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 org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Method; +import java.util.Optional; + +/** + * Inherit QueryMethod class to generate a method that is designated to execute a finder query. + */ +public class CosmosQueryMethod extends QueryMethod { + + private CosmosEntityMetadata metadata; + private final String annotatedQueryValue; + + /** + * 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); + this.annotatedQueryValue = findAnnotatedQuery(method).orElse(null); + } + + @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; + } + + /** + * Returns whether the method has an annotated query. + * + * @return if the query method has an annotated query + */ + public boolean hasAnnotatedQuery() { + return annotatedQueryValue != null; + } + + /** + * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation + * found + * nor the attribute was specified. + * + * @return the query string or null + */ + @Nullable + public String getQueryAnnotation() { + return annotatedQueryValue; + } + + private Optional findAnnotatedQuery(Method method) { + return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class)) + .map(Query::value) + .filter(StringUtils::hasText); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/PartTreeCosmosQuery.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/PartTreeCosmosQuery.java new file mode 100644 index 000000000000..38b9ecac23ee --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/PartTreeCosmosQuery.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.core.mapping.CosmosPersistentProperty; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +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 CosmosQuery createQuery(CosmosParameterAccessor accessor) { + final CosmosQueryCreator creator = new CosmosQueryCreator(tree, accessor, mappingContext); + + final CosmosQuery query = creator.createQuery(); + + if (tree.isLimiting()) { + Integer limit = tree.getMaxResults(); + if (limit != null) { + query.withLimit(limit); + } + } + + return query; + } + + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } + + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } + + @Override + protected boolean isCountQuery() { + return tree.isCountProjection(); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/PartTreeReactiveCosmosQuery.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/PartTreeReactiveCosmosQuery.java new file mode 100644 index 000000000000..52d9fd9bd7d9 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/PartTreeReactiveCosmosQuery.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; +import com.azure.spring.data.cosmos.core.mapping.CosmosPersistentProperty; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +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 CosmosQuery createQuery(ReactiveCosmosParameterAccessor accessor) { + final ReactiveCosmosQueryCreator creator = new ReactiveCosmosQueryCreator(tree, accessor, mappingContext); + + final CosmosQuery query = creator.createQuery(); + + if (tree.isLimiting()) { + throw new UnsupportedOperationException("Limiting is not supported."); + } + + return query; + } + + @Override + protected boolean isDeleteQuery() { + return tree.isDelete(); + } + + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } + + protected boolean isCountQuery() { + return tree.isCountProjection(); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosEntityMetadata.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosEntityMetadata.java new file mode 100644 index 000000000000..91e2505dd262 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosEntityMetadata.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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 container name from the given entity + * @return String + */ + String getContainerName(); +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosParameterAccessor.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosParameterAccessor.java new file mode 100644 index 000000000000..9222078ea1a2 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosParameterAccessor.java @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosParameterParameterAccessor.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosParameterParameterAccessor.java new file mode 100644 index 000000000000..979314e13c46 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosParameterParameterAccessor.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryCreator.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryCreator.java new file mode 100644 index 000000000000..a7c6cd5bc9f5 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryCreator.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.core.mapping.CosmosPersistentProperty; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.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, part.shouldIgnoreCase()); + } + + @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 CosmosQuery complete(@NonNull Criteria criteria, @NonNull Sort sort) { + return new CosmosQuery(criteria).with(sort); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryExecution.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryExecution.java new file mode 100644 index 000000000000..e653dad6364c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryExecution.java @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.exception.CosmosAccessException; +import org.springframework.data.repository.query.ReturnedType; + +/** + * 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(CosmosQuery query, Class type, String container); + + /** + * Container operation implementation to execute a container name query + */ + final class ContainerExecution implements ReactiveCosmosQueryExecution { + + private final ReactiveCosmosOperations operations; + + /** + * Creates a new instance of container execution. + * + * @param operations Reactive Cosmos operations + */ + public ContainerExecution(ReactiveCosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery query, Class type, String container) { + return operations.getContainerName(type); + } + } + + /** + * Find operation implementation to execute a find query for multiple items + */ + final class MultiEntityExecution implements ReactiveCosmosQueryExecution { + + private final ReactiveCosmosOperations operations; + + /** + * Creates a new instance of multi-entity execution. + * + * @param operations Reactive Cosmos operations + */ + public MultiEntityExecution(ReactiveCosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery query, Class type, String container) { + return operations.find(query, type, container); + } + } + + /** + * Find operation implementation to execute a find query for a single item + */ + final class SingleEntityExecution implements ReactiveCosmosQueryExecution { + + private final ReactiveCosmosOperations operations; + private final ReturnedType returnedType; + + /** + * Creates a new instance of single-entity execution. + * + * @param operations Reactive Cosmos operations + * @param returnedType Return type + */ + public SingleEntityExecution(ReactiveCosmosOperations operations, ReturnedType returnedType) { + this.operations = operations; + this.returnedType = returnedType; + } + + @Override + public Object execute(CosmosQuery query, Class type, String container) { + return operations.find(query, type, container) + .buffer(2) + .map((vals) -> { + if (vals.size() > 1) { + throw new CosmosAccessException("Too many results - Expected Mono<" + + returnedType.getReturnedType() + + "> but query returned multiple results"); + } + return vals.iterator().next(); + }); + } + } + + /** + * Exist operation implementation to execute a exist query + */ + final class ExistsExecution implements ReactiveCosmosQueryExecution { + + private final ReactiveCosmosOperations operations; + + /** + * Creates a new instance of exists execution. + * + * @param operations Reactive Cosmos operations + */ + public ExistsExecution(ReactiveCosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery 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; + + /** + * Creates a new instance of delete execution. + * + * @param operations Reactive Cosmos operations + */ + public DeleteExecution(ReactiveCosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery query, Class type, String container) { + return operations.delete(query, type, container); + } + } + + /** + * Count operation implementation to execute a count query + */ + final class CountExecution implements ReactiveCosmosQueryExecution { + + private final ReactiveCosmosOperations operations; + + /** + * Creates a new instance of count execution. + * + * @param operations Reactive Cosmos operations + */ + public CountExecution(ReactiveCosmosOperations operations) { + this.operations = operations; + } + + @Override + public Object execute(CosmosQuery query, Class type, String container) { + return operations.count(query, container); + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryMethod.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryMethod.java new file mode 100644 index 000000000000..5b2755257164 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryMethod.java @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.repository.Query; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.springframework.core.annotation.AnnotatedElementUtils; +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 org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.lang.reflect.Method; +import java.util.Optional; + +/** + * Inherit from QueryMethod class to execute a finder query. + */ +public class ReactiveCosmosQueryMethod extends QueryMethod { + + private ReactiveCosmosEntityMetadata metadata; + private final Method method; + private final String annotatedQueryValue; + + /** + * 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); + this.method = method; + this.annotatedQueryValue = findAnnotatedQuery(method).orElse(null); + } + + @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; + } + + /** + * Returns the reactive wrapper class type if it exists or null otherwise + * + * @return Reactive wrapper class (Flux or Mono) + */ + public Class getReactiveWrapper() { + return isReactiveWrapperClass(method.getReturnType()) ? method.getReturnType() : null; + } + + private static boolean isReactiveWrapperClass(Class clazz) { + return clazz.equals(Flux.class) || clazz.equals(Mono.class); + } + + /** + * Returns whether the method has an annotated query. + * @return if the query method has an annotated query + */ + public boolean hasAnnotatedQuery() { + return annotatedQueryValue != null; + } + + /** + * Gets the annotated query or returns null + * @return the annotated query String or null + */ + @Nullable + public String getQueryAnnotation() { + return annotatedQueryValue; + } + + private Optional findAnnotatedQuery(Method method) { + return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class)) + .map(Query::value) + .filter(StringUtils::hasText); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/SimpleCosmosEntityMetadata.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/SimpleCosmosEntityMetadata.java new file mode 100644 index 000000000000..45658a1df22a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/SimpleCosmosEntityMetadata.java @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/SimpleReactiveCosmosEntityMetadata.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/SimpleReactiveCosmosEntityMetadata.java new file mode 100644 index 000000000000..f1f34110daaf --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/SimpleReactiveCosmosEntityMetadata.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/query/package-info.java new file mode 100644 index 000000000000..7d75b179a800 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.repository.query; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosEntityInformation.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosEntityInformation.java new file mode 100644 index 000000000000..8bd7f2b63bef --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosEntityInformation.java @@ -0,0 +1,711 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.cosmos.models.CompositePath; +import com.azure.cosmos.models.ExcludedPath; +import com.azure.cosmos.models.IncludedPath; +import com.azure.cosmos.models.IndexingMode; +import com.azure.cosmos.models.IndexingPolicy; +import com.azure.cosmos.models.UniqueKey; +import com.azure.cosmos.models.UniqueKeyPolicy; +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.common.Memoizer; +import com.azure.spring.data.cosmos.core.mapping.CompositeIndex; +import com.azure.spring.data.cosmos.core.mapping.CompositeIndexPath; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.CosmosUniqueKey; +import com.azure.spring.data.cosmos.core.mapping.CosmosUniqueKeyPolicy; +import com.azure.spring.data.cosmos.core.mapping.GeneratedValue; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.domain.Persistable; +import org.springframework.data.repository.core.support.AbstractEntityInformation; +import org.springframework.lang.NonNull; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.function.Function; + +import static com.azure.spring.data.cosmos.common.ExpressionResolver.resolveExpression; + +/** + * Class to describe cosmosDb entity + */ +public class CosmosEntityInformation extends AbstractEntityInformation { + + private static final Function, CosmosEntityInformation> ENTITY_INFORMATION_CREATOR = + Memoizer.memoize(CosmosEntityInformation::getCosmosEntityInformation); + + private static CosmosEntityInformation getCosmosEntityInformation(Class domainClass) { + return new CosmosEntityInformation<>(domainClass); + } + + /** + * Static Factory + * + * @param domainClass to specify id field + * @return new CosmosEntityInformation + */ + public static CosmosEntityInformation getInstance(Class domainClass) { + return ENTITY_INFORMATION_CREATOR.apply(domainClass); + } + + private final Field id; + private final Field partitionKeyField; + private final Field versionField; + private final String containerName; + private final String partitionKeyPath; + private final Integer requestUnit; + private final Integer timeToLive; + private final IndexingPolicy indexingPolicy; + private final UniqueKeyPolicy uniqueKeyPolicy; + private final boolean autoCreateContainer; + private final boolean autoGenerateId; + private final boolean persitable; + private final boolean autoScale; + private final boolean isIndexingPolicySpecified; + private final boolean overwriteIndexingPolicy; + + /** + * Initialization + * + * @param domainType to specify id field + */ + public CosmosEntityInformation(Class domainType) { + super(domainType); + + this.id = CosmosEntityInformationHelper.getIdField(domainType, getJavaType()); + ReflectionUtils.makeAccessible(this.id); + + this.autoGenerateId = CosmosEntityInformationHelper.isIdFieldAnnotatedWithGeneratedValue(this.id); + + this.containerName = CosmosEntityInformationHelper.getContainerName(domainType); + this.partitionKeyPath = CosmosEntityInformationHelper.getPartitionKeyPathAnnotationValue(domainType); + + this.partitionKeyField = CosmosEntityInformationHelper.getPartitionKeyField(domainType); + if (this.partitionKeyField != null) { + ReflectionUtils.makeAccessible(this.partitionKeyField); + } + + this.versionField = CosmosEntityInformationHelper.getVersionedField(domainType); + if (this.versionField != null) { + ReflectionUtils.makeAccessible(this.versionField); + } + + this.requestUnit = CosmosEntityInformationHelper.getRequestUnit(domainType); + this.timeToLive = CosmosEntityInformationHelper.getTimeToLive(domainType); + this.indexingPolicy = CosmosEntityInformationHelper.getIndexingPolicy(domainType); + this.uniqueKeyPolicy = CosmosEntityInformationHelper.getUniqueKeyPolicy(domainType); + this.autoCreateContainer = CosmosEntityInformationHelper.getIsAutoCreateContainer(domainType); + this.persitable = Persistable.class.isAssignableFrom(domainType); + this.autoScale = CosmosEntityInformationHelper.getIsAutoScale(domainType); + this.isIndexingPolicySpecified = CosmosEntityInformationHelper.isIndexingPolicySpecified(domainType); + this.overwriteIndexingPolicy = CosmosEntityInformationHelper.getIndexingPolicyOverwritePolicy(domainType); + } + + @Override + public boolean isNew(T entity) { + if (persitable) { + return ((Persistable) entity).isNew(); + } else { + return super.isNew(entity); + } + } + + /** + * 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 field name + * + * @return string + */ + public String getIdFieldName() { + return id.getName(); + } + + /** + * Should generate Id field value + * + * @return boolean + */ + public boolean shouldGenerateId() { + return autoGenerateId; + } + + /** + * Get id type + * + * @return class of id type + */ + @SuppressWarnings("unchecked") + public Class getIdType() { + return (Class) id.getType(); + } + + /** + * 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; + } + + /** + * Gets the UniqueKeyPolicy + * @return UniqueKeyPolicy + */ + public UniqueKeyPolicy getUniqueKeyPolicy() { + return uniqueKeyPolicy; + } + + /** + * Check if is versioned + * + * @return boolean + */ + public boolean isVersioned() { + return versionField != null; + } + + /** + * Get name of field annotated with @Version if any + * + * @return String + */ + public String getVersionFieldName() { + return versionField == null ? null : versionField.getName(); + } + + /** + * Get the computed partition key path for container + * + * @return partition key path + */ + public String getPartitionKeyPath() { + if (partitionKeyField == null) { + return partitionKeyPath == null ? "/null" : partitionKeyPath; + } else { + final PartitionKey partitionKey = partitionKeyField.getAnnotation(PartitionKey.class); + return partitionKey.value().equals("") ? "/" + partitionKeyField.getName() : "/" + partitionKey.value(); + } + } + + /** + * Get the value of the field marked as the version field + * + * @param entity the object to get the value from + * @return the value of the version field + */ + public String getVersionFieldValue(Object entity) { + return versionField == null ? null : (String) ReflectionUtils.getField(versionField, entity); + } + + /** + * 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 Object getPartitionKeyFieldValue(T entity) { + return partitionKeyField == null ? null : ReflectionUtils.getField(partitionKeyField, entity); + } + + /** + * @return the partition key field name + */ + public String getPartitionKeyFieldName() { + return partitionKeyField == null ? null : partitionKeyField.getName(); + } + + /** + * Check if auto creating container is allowed + * + * @return boolean + */ + public boolean isAutoCreateContainer() { + return autoCreateContainer; + } + + /** + * Check if overwrite indexing policy is enabled + * + * @return boolean + */ + public boolean isOverwriteIndexingPolicy() { + return overwriteIndexingPolicy; + } + + /** + * Check if container should use autoscale for resource units + * + * @return boolean + */ + public boolean isAutoScale() { + return autoScale; + } + + /** + * @return whether indexing policy is specified + */ + public boolean isIndexingPolicySpecified() { + return this.isIndexingPolicySpecified; + } + + /** + * Class of helper functions for CosmosEntityInformation to call from the constructor + */ + static class CosmosEntityInformationHelper { + + /** + * Gets if the indexing policy is specified for the entity + * + * @param domainType the domain type + * @return boolean + */ + private static boolean isIndexingPolicySpecified(Class domainType) { + return domainType.getAnnotation(CosmosIndexingPolicy.class) != null; + } + + /** + * Gets the indexing policy of the entity + * + * @param domainType the domain type + * @return IndexingPolicy + */ + private static IndexingPolicy getIndexingPolicy(Class domainType) { + final IndexingPolicy policy = new IndexingPolicy(); + + policy.setAutomatic(getIndexingPolicyAutomatic(domainType)); + policy.setIndexingMode(getIndexingPolicyMode(domainType)); + policy.setIncludedPaths(getIndexingPolicyIncludePaths(domainType)); + policy.setExcludedPaths(getIndexingPolicyExcludePaths(domainType)); + policy.setCompositeIndexes(getIndexingPolicyCompositeIndexes(domainType)); + + return policy; + } + + /** + * Gets the unique key policy of the entity + * + * @param domainType the domain type + * @return UniqueKeyPolicy + */ + private static UniqueKeyPolicy getUniqueKeyPolicy(Class domainType) { + CosmosUniqueKeyPolicy annotation = domainType.getAnnotation(CosmosUniqueKeyPolicy.class); + if (annotation == null) { + return null; + } + + List uniqueKeys = getUniqueKeys(domainType); + if (uniqueKeys.isEmpty()) { + return null; + } + return new UniqueKeyPolicy().setUniqueKeys(uniqueKeys); + } + + /** + * Gets the id field of the entity + * + * @param domainType the domain type + * @param javaType the java type of the domain class + * @return Field id + * @throws IllegalArgumentException if the id field fails validation + */ + private static Field getIdField(Class domainType, Class javaType) { + final Field idField; + final List fields = FieldUtils.getFieldsListWithAnnotation(domainType, Id.class); + + if (fields.isEmpty()) { + idField = ReflectionUtils.findField(javaType, 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 + && idField.getType() != Long.class + && idField.getType() != long.class + && idField.getType() != UUID.class) { + throw new IllegalArgumentException("type of id field must be String, Integer, Long or UUID"); + } + + return idField; + } + + /** + * Gets if the id field is annotated with generated value for the entity + * + * @param idField the id of the entity + * @return boolean + * @throws IllegalArgumentException if the id field fails validation + */ + private static boolean isIdFieldAnnotatedWithGeneratedValue(Field idField) { + if (idField.getAnnotation(GeneratedValue.class) != null) { + if (idField.getType() == String.class) { + return true; + } else { + throw new IllegalArgumentException("id field must be of type String if " + + "GeneratedValue annotation is present"); + } + } + return false; + } + + /** + * Gets the container name of the entity + * + * @param domainType the domain type + * @return String container name + */ + private static String getContainerName(Class domainType) { + String customContainerName = domainType.getSimpleName(); + + final Container annotation = domainType.getAnnotation(Container.class); + + if (annotation != null && !annotation.containerName().isEmpty()) { + customContainerName = resolveExpression(annotation.containerName()); + } + + return customContainerName; + } + + /** + * Gets the partition key path of the entity + * + * @param domainType the domain type + * @return String partition key path + */ + private static String getPartitionKeyPathAnnotationValue(Class domainType) { + final Container annotation = domainType.getAnnotation(Container.class); + + if (annotation != null && !annotation.partitionKeyPath().isEmpty()) { + return annotation.partitionKeyPath(); + } + return null; + } + + /** + * Gets the partition key of the entity + * + * @param domainType the domain type + * @return Field partition key + * @throws IllegalArgumentException if the partition key field fails validation + */ + private static 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!"); + } + return partitionKey; + } + + /** + * Gets the request units for the entity + * + * @param domainType the domain type + * @return Integer ru's + */ + private static Integer getRequestUnit(Class domainType) { + Integer ru = null; + final Container annotation = domainType.getAnnotation(Container.class); + + if (annotation != null + && annotation.ru() != null + && !annotation.ru().isEmpty()) { + ru = Integer.parseInt(annotation.ru()); + } + return ru; + } + + /** + * Gets the time to live for the entity + * + * @param domainType the domain type + * @return Integer ttl + */ + private static Integer getTimeToLive(Class domainType) { + Integer ttl = Constants.DEFAULT_TIME_TO_LIVE; + final Container annotation = domainType.getAnnotation(Container.class); + + if (annotation != null) { + ttl = annotation.timeToLive(); + } + + return ttl; + } + + /** + * Gets if we overwrite the indexing policy in the portal for the entity + * + * @param domainType the domain type + * @return boolean + */ + private static boolean getIndexingPolicyOverwritePolicy(Class domainType) { + boolean isOverwritePolicy = Constants.DEFAULT_INDEXING_POLICY_OVERWRITE_POLICY; + final CosmosIndexingPolicy annotation = domainType.getAnnotation(CosmosIndexingPolicy.class); + + if (annotation != null) { + isOverwritePolicy = annotation.overwritePolicy(); + } + + return isOverwritePolicy; + } + + /** + * Gets if automatic is defined on the indexing policy for the entity + * + * @param domainType the domain type + * @return boolean + */ + private static boolean getIndexingPolicyAutomatic(Class domainType) { + boolean isAutomatic = Constants.DEFAULT_INDEXING_POLICY_AUTOMATIC; + final CosmosIndexingPolicy annotation = domainType.getAnnotation(CosmosIndexingPolicy.class); + + if (annotation != null) { + isAutomatic = annotation.automatic(); + } + + return isAutomatic; + } + + /** + * Gets the indexing policy mode for the entity + * + * @param domainType the domain type + * @return IndexingMode + */ + private static IndexingMode getIndexingPolicyMode(Class domainType) { + IndexingMode mode = Constants.DEFAULT_INDEXING_POLICY_MODE; + final CosmosIndexingPolicy annotation = domainType.getAnnotation(CosmosIndexingPolicy.class); + + if (annotation != null) { + mode = annotation.mode(); + } + + return mode; + } + + /** + * Gets the include paths from the indexing policy for the entity + * + * @param domainType the domain type + * @return List list of IncludePath's + */ + private static List getIndexingPolicyIncludePaths(Class domainType) { + final List pathArrayList = new ArrayList<>(); + final CosmosIndexingPolicy annotation = domainType.getAnnotation(CosmosIndexingPolicy.class); + + if (annotation == 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; + } + + /** + * Gets the exclude paths from the indexing policy for the entity + * + * @param domainType the domain type + * @return List list of ExcludePath's + */ + private static List getIndexingPolicyExcludePaths(Class domainType) { + final List pathArrayList = new ArrayList<>(); + final CosmosIndexingPolicy annotation = domainType.getAnnotation(CosmosIndexingPolicy.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) { + pathArrayList.add(new ExcludedPath(path)); + } + + return pathArrayList; + } + + /** + * Gets the composite indexes from the indexing policy for the entity + * + * @param domainType the domain type + * @return List List of compositepath's + */ + private static List> getIndexingPolicyCompositeIndexes(Class domainType) { + final List> compositePathList = new ArrayList<>(); + final CosmosIndexingPolicy annotation = domainType.getAnnotation(CosmosIndexingPolicy.class); + + if (annotation == null || annotation.compositeIndexes().length == 0) { + return Collections.emptyList(); + } + + final CompositeIndex[] compositeIndexes = annotation.compositeIndexes(); + for (final CompositeIndex index: compositeIndexes) { + final List paths = new ArrayList<>(); + compositePathList.add(paths); + for (final CompositeIndexPath path : index.paths()) { + CompositePath compositePath = new CompositePath(); + compositePath.setPath(path.path()); + compositePath.setOrder(path.order()); + paths.add(compositePath); + } + } + + return compositePathList; + } + + /** + * Gets the unique keys for the entity + * + * @param domainType the domain type + * @return List list of UniqueKey's + */ + private static List getUniqueKeys(Class domainType) { + CosmosUniqueKeyPolicy annotation = domainType.getAnnotation(CosmosUniqueKeyPolicy.class); + assert annotation != null; + if (annotation.uniqueKeys().length == 0) { + return Collections.emptyList(); + } + final CosmosUniqueKey[] uniqueKeysPath = annotation.uniqueKeys(); + final List uniqueKeys = new ArrayList<>(); + for (final CosmosUniqueKey uniqueKey : uniqueKeysPath) { + UniqueKey key = new UniqueKey(Arrays.asList(uniqueKey.paths())); + uniqueKeys.add(key); + } + return uniqueKeys; + } + + /** + * Gets the versioned field from the entity + * + * @param domainClass the domain class + * @return Field version + * @throws IllegalArgumentException if the version field fails validation + */ + private static Field getVersionedField(Class domainClass) { + Field version = null; + final List fields = FieldUtils.getFieldsListWithAnnotation(domainClass, Version.class); + + if (fields.size() == 1) { + version = fields.get(0); + } else if (fields.size() > 1) { + throw new IllegalArgumentException("Azure Cosmos DB supports only one field with @Version annotation!"); + } + + if (version != null && version.getType() != String.class) { + throw new IllegalArgumentException("type of Version field must be String"); + } + return version; + } + + /** + * Gets if the entity is set to auto create + * + * @param domainType the domain type + * @return boolean + */ + private static boolean getIsAutoCreateContainer(Class domainType) { + final Container annotation = domainType.getAnnotation(Container.class); + + boolean autoCreateContainer = Constants.DEFAULT_AUTO_CREATE_CONTAINER; + if (annotation != null) { + autoCreateContainer = annotation.autoCreateContainer(); + } + + return autoCreateContainer; + } + + /** + * Gets if the entity is set to auto scale + * + * @param domainType the domain type + * @return boolean + */ + private static boolean getIsAutoScale(Class domainType) { + final Container annotation = domainType.getAnnotation(Container.class); + + boolean autoScale = Constants.DEFAULT_AUTO_SCALE; + if (annotation != null) { + autoScale = annotation.autoScale(); + } + + return autoScale; + } + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactory.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactory.java new file mode 100644 index 000000000000..ec70b15aad30 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactory.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.repository.query.CosmosQueryMethod; +import com.azure.spring.data.cosmos.repository.query.PartTreeCosmosQuery; +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 CosmosOperations cosmosOperations; + + /** + * Initialization + * + * @param cosmosOperations for cosmosDb operations + */ + public CosmosRepositoryFactory(CosmosOperations cosmosOperations) { + this.cosmosOperations = cosmosOperations; + } + + @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.cosmosOperations); + } + + @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!"); + + if (queryMethod.hasAnnotatedQuery()) { + return new StringBasedCosmosQuery(queryMethod, dbOperations); + } else { + return new PartTreeCosmosQuery(queryMethod, dbOperations); + } + + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryBean.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryBean.java new file mode 100644 index 000000000000..46e3cc2d8c6c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryBean.java @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import org.springframework.beans.factory.FactoryBean; +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 { + + 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 + */ + public void setCosmosOperations(CosmosOperations operations) { + this.operations = operations; + } + + @Override + protected final RepositoryFactorySupport createRepositoryFactory() { + return getFactoryInstance(); + } + + /** + * @return the factory instance + */ + protected RepositoryFactorySupport getFactoryInstance() { + return new CosmosRepositoryFactory(operations); + } + + @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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/IndexPolicyCompareService.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/IndexPolicyCompareService.java new file mode 100644 index 000000000000..cd8fbc31134d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/IndexPolicyCompareService.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.cosmos.models.ExcludedPath; +import com.azure.cosmos.models.IncludedPath; +import com.azure.cosmos.models.IndexingPolicy; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Class for determining if the index policy currently applied to the container matches the index policy that is + * specified on an entities' @CosmosIndexingPolicy annotation + */ +public class IndexPolicyCompareService { + + /** + * Whether the policy needs updating. + * + * @param existingPolicy Existing policy + * @param newPolicy New policy + * @return Whether the policy needs updating. + */ + public static boolean policyNeedsUpdate(IndexingPolicy existingPolicy, IndexingPolicy newPolicy) { + return !hasSameIncludedPaths(existingPolicy.getIncludedPaths(), newPolicy.getIncludedPaths()) + || !hasSameExcludedPaths(existingPolicy.getExcludedPaths(), newPolicy.getExcludedPaths()) + || !existingPolicy.getCompositeIndexes().equals(newPolicy.getCompositeIndexes()) + || !existingPolicy.getIndexingMode().equals(newPolicy.getIndexingMode()) + || !existingPolicy.isAutomatic().equals(newPolicy.isAutomatic()); + } + + // Returns true if the lists are the same or the only difference is that the existing paths contain "/*" + private static boolean hasSameIncludedPaths(List existingPaths, List newPaths) { + List existingListDiff = existingPaths.stream() + .filter(element -> !newPaths.contains(element)) + .collect(Collectors.toList()); + + List newListDiff = newPaths.stream() + .filter(element -> !existingPaths.contains(element)) + .collect(Collectors.toList()); + + return (existingListDiff.size() == 0 && newListDiff.size() == 0) + || (newListDiff.size() == 0 + && existingListDiff.size() == 1 + && existingListDiff.get(0).getPath().equals("/*")); + } + + // Returns true if the lists are the same or the only difference is that the existing paths contain the etag field + private static boolean hasSameExcludedPaths(List existingPaths, List newPaths) { + List existingListDiff = existingPaths.stream() + .filter(element -> !newPaths.contains(element)) + .collect(Collectors.toList()); + + List newListDiff = newPaths.stream() + .filter(element -> !existingPaths.contains(element)) + .collect(Collectors.toList()); + + return (existingListDiff.size() == 0 && newListDiff.size() == 0) + || (newListDiff.size() == 0 + && existingListDiff.size() == 1 + && existingListDiff.get(0).getPath().equals("/\"_etag\"/?")); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactory.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactory.java new file mode 100644 index 000000000000..ad04d04b21fb --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactory.java @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; +import com.azure.spring.data.cosmos.repository.query.PartTreeReactiveCosmosQuery; +import com.azure.spring.data.cosmos.repository.query.ReactiveCosmosQueryMethod; +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 ReactiveCosmosOperations cosmosOperations; + + /** + * Initialization + * + * @param cosmosOperations for cosmosDB operations + */ + public ReactiveCosmosRepositoryFactory(ReactiveCosmosOperations cosmosOperations) { + 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.cosmosOperations); + } + + @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!"); + if (queryMethod.hasAnnotatedQuery()) { + return new StringBasedReactiveCosmosQuery(queryMethod, cosmosOperations); + } else { + return new PartTreeReactiveCosmosQuery(queryMethod, cosmosOperations); + } + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactoryBean.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactoryBean.java new file mode 100644 index 000000000000..d46cc99ade06 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactoryBean.java @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import org.springframework.beans.factory.FactoryBean; +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 { + + 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 + */ + public void setReactiveCosmosOperations(ReactiveCosmosOperations operations) { + this.cosmosOperations = operations; + } + + @Override + protected final RepositoryFactorySupport createRepositoryFactory() { + return getFactoryInstance(); + } + + /** + * @return the factory instance + */ + protected RepositoryFactorySupport getFactoryInstance() { + return new ReactiveCosmosRepositoryFactory(cosmosOperations); + } + + @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/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleCosmosRepository.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleCosmosRepository.java new file mode 100644 index 000000000000..debea67b73f3 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleCosmosRepository.java @@ -0,0 +1,327 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.cosmos.CosmosException; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +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; + +import static com.azure.spring.data.cosmos.repository.support.IndexPolicyCompareService.policyNeedsUpdate; + +/** + * 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 dbOperations for cosmosDB operation + */ + public SimpleCosmosRepository(CosmosEntityInformation metadata, + CosmosOperations dbOperations) { + this.operation = dbOperations; + this.information = metadata; + + if (this.information.isAutoCreateContainer()) { + createContainerIfNotExists(); + } + + if (this.information.isOverwriteIndexingPolicy()) { + overwriteIndexingPolicy(); + } + } + + private void overwriteIndexingPolicy() { + CosmosContainerProperties currentProperties = getContainerProperties(); + if (currentProperties != null + && policyNeedsUpdate(currentProperties.getIndexingPolicy(), information.getIndexingPolicy())) { + currentProperties.setIndexingPolicy(information.getIndexingPolicy()); + replaceContainerProperties(currentProperties); + } + } + + private CosmosContainerProperties getContainerProperties() { + try { + return this.operation.getContainerProperties(this.information.getContainerName()); + } catch (CosmosException ex) { + if (ex.getStatusCode() == 404) { + return null; + } else { + throw ex; + } + } + } + + private CosmosContainerProperties replaceContainerProperties(CosmosContainerProperties properties) { + return this.operation.replaceContainerProperties(this.information.getContainerName(), properties); + } + + 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); + } else { + return operation.upsertAndReturnEntity(information.getContainerName(), entity); + } + } + + /** + * patch entity with CosmosPatchItemRequestOptions + * @param id of entity to be patched + * @param partitionKey of entity to be patched + * @param patchOperations for entity to be patched + * @param domainType of entity + */ + @Override + public S save(ID id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations) { + Assert.notNull(id, "id must not be null"); + Assert.notNull(partitionKey, "partitionKey must not be null"); + // patch items + return operation.patch(id, partitionKey, domainType, patchOperations); + } + + /** + * patch entity with CosmosPatchItemRequestOptions + * @param id of entity to be patched + * @param partitionKey of entity to be patched + * @param patchOperations for entity to be patched + * @param options options + * @param domainType of entity + */ + @Override + public S save(ID id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options) { + Assert.notNull(id, "id must not be null"); + Assert.notNull(partitionKey, "partitionKey must not be null"); + // patch items + return operation.patch(id, partitionKey, domainType, patchOperations, options); + } + + /** + * 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 Iterable 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"); + + operation.deleteEntity(information.getContainerName(), entity); + } + + @Override + public void deleteAllById(Iterable ids) { + Assert.notNull(ids, "Iterable entities should not be null"); + StreamSupport.stream(ids.spliterator(), true).forEach(this::deleteById); + } + + /** + * 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 CosmosQuery query = + new CosmosQuery(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 Iterable findAll(PartitionKey partitionKey) { + return operation.findAll(partitionKey, information.getJavaType()); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleReactiveCosmosRepository.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleReactiveCosmosRepository.java new file mode 100644 index 000000000000..3a8202969fe1 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/SimpleReactiveCosmosRepository.java @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.cosmos.CosmosException; +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.CosmosContainerResponse; +import com.azure.cosmos.models.CosmosPatchItemRequestOptions; +import com.azure.cosmos.models.CosmosPatchOperations; +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Sort; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.Serializable; + +import static com.azure.spring.data.cosmos.repository.support.IndexPolicyCompareService.policyNeedsUpdate; + +/** + * Repository class for simple reactive Cosmos operation + */ +public class SimpleReactiveCosmosRepository implements ReactiveCosmosRepository { + + private final CosmosEntityInformation entityInformation; + private final ReactiveCosmosOperations cosmosOperations; + + /** + * 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(); + } + + if (this.entityInformation.isOverwriteIndexingPolicy()) { + overwriteIndexingPolicy(); + } + } + + private void overwriteIndexingPolicy() { + CosmosContainerProperties currentProperties = getContainerProperties(); + if (currentProperties != null + && policyNeedsUpdate(currentProperties.getIndexingPolicy(), entityInformation.getIndexingPolicy())) { + currentProperties.setIndexingPolicy(entityInformation.getIndexingPolicy()); + replaceContainerProperties(currentProperties); + } + } + + private CosmosContainerProperties getContainerProperties() { + try { + return this.cosmosOperations.getContainerProperties(this.entityInformation.getContainerName()).block(); + } catch (CosmosException ex) { + if (ex.getStatusCode() == 404) { + return null; + } else { + throw ex; + } + } + } + + private CosmosContainerProperties replaceContainerProperties(CosmosContainerProperties properties) { + return this.cosmosOperations.replaceContainerProperties(this.entityInformation.getContainerName(), properties) + .block(); + } + + 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 CosmosQuery query = + new CosmosQuery(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); + } else { + return cosmosOperations.upsert(entityInformation.getContainerName(), entity); + } + } + + @Override + public Mono save(K id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations) { + Assert.notNull(id, "entity must not be null"); + // patch items + return cosmosOperations.patch(id, partitionKey, domainType, patchOperations); + } + + @Override + public Mono save(K id, PartitionKey partitionKey, Class domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options) { + Assert.notNull(id, "entity must not be null"); + // patch items + return cosmosOperations.patch(id, partitionKey, domainType, patchOperations, options); + } + + @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!"); + + return cosmosOperations.deleteEntity(entityInformation.getContainerName(), entity); + } + + @Override + public Mono deleteAllById(Iterable ids) { + Assert.notNull(ids, "The given Iterable of ids must not be null!"); + return Flux.fromIterable(ids).flatMap(this::deleteById).then(); + } + + @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.getJavaType()); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedCosmosQuery.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedCosmosQuery.java new file mode 100644 index 000000000000..baa27113e679 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedCosmosQuery.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.repository.query.AbstractCosmosQuery; +import com.azure.spring.data.cosmos.repository.query.CosmosEntityMetadata; +import com.azure.spring.data.cosmos.repository.query.CosmosParameterAccessor; +import com.azure.spring.data.cosmos.repository.query.CosmosParameterParameterAccessor; +import com.azure.spring.data.cosmos.repository.query.CosmosQueryMethod; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.ResultProcessor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter.toCosmosDbValue; + +/** + * Cosmos query class to handle the annotated queries. This overrides the execution and runs the query directly + */ +public class StringBasedCosmosQuery extends AbstractCosmosQuery { + private static final Pattern COUNT_QUERY_PATTERN = Pattern.compile("^\\s*select\\s+value\\s+count.*", Pattern.CASE_INSENSITIVE); + + private final String query; + + /** + * Constructor + * @param queryMethod the CosmosQueryMethod + * @param dbOperations the CosmosOperations + */ + public StringBasedCosmosQuery(CosmosQueryMethod queryMethod, CosmosOperations dbOperations) { + super(queryMethod, dbOperations); + this.query = queryMethod.getQueryAnnotation(); + } + + @Override + protected CosmosQuery createQuery(CosmosParameterAccessor accessor) { + return null; + } + + @Override + public Object execute(final Object[] parameters) { + final CosmosParameterAccessor accessor = new CosmosParameterParameterAccessor(getQueryMethod(), parameters); + final ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + + /* + * The below for loop is used to handle two unique use cases with annotated queries. + * Annotated queries are defined as strings so there is no way to know the clauses + * being used in advance. Some clauses expect an array and others expect just a list of values. + * (1) IN clauses expect the syntax 'IN (a, b, c) which is generated from the if statement. + * (2) ARRAY_CONTAINS expects the syntax 'ARRAY_CONTAINS(["a", "b", "c"], table.param) which + * is generated from the else statement. + */ + String expandedQuery = query; + List sqlParameters = new ArrayList<>(); + String modifiedExpandedQuery = expandedQuery.toLowerCase(Locale.US).replaceAll("\\s+", ""); + for (int paramIndex = 0; paramIndex < parameters.length; paramIndex++) { + Parameter queryParam = getQueryMethod().getParameters().getParameter(paramIndex); + String paramName = queryParam.getName().orElse(""); + if (!("").equals(paramName)) { + String inParamCheck = "array_contains(@" + paramName.toLowerCase(Locale.US); + if (parameters[paramIndex] instanceof Collection && !modifiedExpandedQuery.contains(inParamCheck)) { + List expandParam = ((Collection) parameters[paramIndex]).stream() + .map(Object::toString).collect(Collectors.toList()); + List expandedParamKeys = new ArrayList<>(); + for (int arrayIndex = 0; arrayIndex < expandParam.size(); arrayIndex++) { + expandedParamKeys.add("@" + paramName + arrayIndex); + sqlParameters.add(new SqlParameter("@" + paramName + arrayIndex, toCosmosDbValue(expandParam.get(arrayIndex)))); + } + expandedQuery = expandedQuery.replaceAll("@" + queryParam.getName().orElse(""), String.join(",", expandedParamKeys)); + } else { + if (!Pageable.class.isAssignableFrom(queryParam.getType()) + && !Sort.class.isAssignableFrom(queryParam.getType())) { + sqlParameters.add(new SqlParameter("@" + queryParam.getName().orElse(""), toCosmosDbValue(parameters[paramIndex]))); + } + } + } + } + + SqlQuerySpec querySpec = new SqlQuerySpec(expandedQuery, sqlParameters); + if (isPageQuery()) { + return this.operations.runPaginationQuery(querySpec, accessor.getPageable(), processor.getReturnedType().getDomainType(), + processor.getReturnedType().getReturnedType()); + } else if (isSliceQuery()) { + return this.operations.runSliceQuery( + querySpec, + accessor.getPageable(), + processor.getReturnedType().getDomainType(), + processor.getReturnedType().getReturnedType()); + } else if (isCountQuery()) { + final String container = ((CosmosEntityMetadata) getQueryMethod().getEntityInformation()).getContainerName(); + return this.operations.count(querySpec, container); + } else { + return this.operations.runQuery(querySpec, accessor.getSort(), processor.getReturnedType().getDomainType(), + processor.getReturnedType().getReturnedType()); + } + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + protected boolean isCountQuery() { + return isCountQuery(query, getQueryMethod().getReturnedObjectType()); + } + + static boolean isCountQuery(String query, Class returnedType) { + if (isCountQueryReturnType(returnedType)) { + return COUNT_QUERY_PATTERN.matcher(query).matches(); + } else { + return false; + } + } + + private static boolean isCountQueryReturnType(Class returnedType) { + return returnedType == Long.class + || returnedType == long.class + || returnedType == Integer.class + || returnedType == int.class; + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedReactiveCosmosQuery.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedReactiveCosmosQuery.java new file mode 100644 index 000000000000..5df429898fae --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedReactiveCosmosQuery.java @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.repository.query.AbstractReactiveCosmosQuery; +import com.azure.spring.data.cosmos.repository.query.ReactiveCosmosParameterAccessor; +import com.azure.spring.data.cosmos.repository.query.ReactiveCosmosParameterParameterAccessor; +import com.azure.spring.data.cosmos.repository.query.ReactiveCosmosQueryMethod; +import com.azure.spring.data.cosmos.repository.query.SimpleReactiveCosmosEntityMetadata; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.ResultProcessor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import static com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter.toCosmosDbValue; + +/** + * Cosmos query class to handle the annotated queries. This overrides the execution and runs the query directly + */ +public class StringBasedReactiveCosmosQuery extends AbstractReactiveCosmosQuery { + + private final String query; + + /** + * Constructor + * @param queryMethod the query method + * @param dbOperations the reactive cosmos operations + */ + public StringBasedReactiveCosmosQuery(ReactiveCosmosQueryMethod queryMethod, + ReactiveCosmosOperations dbOperations) { + super(queryMethod, dbOperations); + this.query = queryMethod.getQueryAnnotation(); + } + + @Override + protected CosmosQuery createQuery(ReactiveCosmosParameterAccessor accessor) { + return null; + } + + @Override + public Object execute(final Object[] parameters) { + final ReactiveCosmosParameterAccessor accessor = new ReactiveCosmosParameterParameterAccessor(getQueryMethod(), + parameters); + final ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + + /* + * The below for loop is used to handle two unique use cases with annotated queries. + * Annotated queries are defined as strings so there is no way to know the clauses + * being used in advance. Some clauses expect an array and others expect just a list of values. + * (1) IN clauses expect the syntax 'IN (a, b, c) which is generated from the if statement. + * (2) ARRAY_CONTAINS expects the syntax 'ARRAY_CONTAINS(["a", "b", "c"], table.param) which + * is generated from the else statement. + */ + String expandedQuery = query; + List sqlParameters = new ArrayList<>(); + String modifiedExpandedQuery = expandedQuery.toLowerCase(Locale.US).replaceAll("\\s+", ""); + for (int paramIndex = 0; paramIndex < parameters.length; paramIndex++) { + Parameter queryParam = getQueryMethod().getParameters().getParameter(paramIndex); + String paramName = queryParam.getName().orElse(""); + if (!("").equals(paramName)) { + String inParamCheck = "array_contains(@" + paramName.toLowerCase(Locale.US); + if (parameters[paramIndex] instanceof Collection && !modifiedExpandedQuery.contains(inParamCheck)) { + ArrayList expandParam = (ArrayList) ((Collection) parameters[paramIndex]).stream() + .map(Object::toString).collect(Collectors.toList()); + List expandedParamKeys = new ArrayList<>(); + for (int arrayIndex = 0; arrayIndex < expandParam.size(); arrayIndex++) { + expandedParamKeys.add("@" + paramName + arrayIndex); + sqlParameters.add(new SqlParameter("@" + paramName + arrayIndex, toCosmosDbValue(expandParam.get(arrayIndex)))); + } + expandedQuery = expandedQuery.replaceAll("@" + queryParam.getName().orElse(""), String.join(",", expandedParamKeys)); + } else { + if (!Pageable.class.isAssignableFrom(queryParam.getType()) + && !Sort.class.isAssignableFrom(queryParam.getType())) { + sqlParameters.add(new SqlParameter("@" + queryParam.getName().orElse(""), toCosmosDbValue(parameters[paramIndex]))); + } + } + } + } + + SqlQuerySpec querySpec = new SqlQuerySpec(expandedQuery, sqlParameters); + if (isCountQuery()) { + final String container = ((SimpleReactiveCosmosEntityMetadata) getQueryMethod().getEntityInformation()).getContainerName(); + final Mono mono = this.operations.count(querySpec, container); + return mono; + } else { + Flux flux = this.operations.runQuery(querySpec, accessor.getSort(), processor.getReturnedType().getDomainType(), + processor.getReturnedType().getReturnedType()); + return flux; + } + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + protected boolean isCountQuery() { + return StringBasedCosmosQuery.isCountQuery(query, getQueryMethod().getReturnedObjectType()); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/package-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/repository/support/package-info.java new file mode 100644 index 000000000000..9e71adafb2cd --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/com/azure/spring/data/cosmos/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.azure.spring.data.cosmos.repository.support; diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/java/module-info.java b/sdk/spring/azure-spring-data-cosmos/src/main/java/module-info.java new file mode 100644 index 000000000000..6a12a4355d20 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/java/module-info.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +module com.azure.spring.data.cosmos { + + requires transitive com.azure.cosmos; + requires com.fasterxml.jackson.datatype.jdk8; + requires com.fasterxml.jackson.module.paramnames; + requires java.desktop; + requires org.apache.commons.lang3; + requires org.slf4j; + requires spring.beans; + requires spring.context; + requires spring.core; + requires spring.data.commons; + requires spring.expression; + requires spring.tx; + + exports com.azure.spring.data.cosmos; + exports com.azure.spring.data.cosmos.common; + exports com.azure.spring.data.cosmos.config; + exports com.azure.spring.data.cosmos.core.convert; + exports com.azure.spring.data.cosmos.core; + exports com.azure.spring.data.cosmos.core.mapping; + exports com.azure.spring.data.cosmos.exception; + exports com.azure.spring.data.cosmos.repository; + exports com.azure.spring.data.cosmos.repository.config; + exports com.azure.spring.data.cosmos.repository.support; + + opens com.azure.spring.data.cosmos.config to spring.core; + opens com.azure.spring.data.cosmos.core.mapping to spring.core; + opens com.azure.spring.data.cosmos.repository.support to spring.beans; +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/resources/META-INF/azure-spring-data-cosmos.properties b/sdk/spring/azure-spring-data-cosmos/src/main/resources/META-INF/azure-spring-data-cosmos.properties new file mode 100644 index 000000000000..90d42083480f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/resources/META-INF/azure-spring-data-cosmos.properties @@ -0,0 +1 @@ +project.version=@project.version@ diff --git a/sdk/spring/azure-spring-data-cosmos/src/main/resources/META-INF/spring.factories b/sdk/spring/azure-spring-data-cosmos/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..2b697a960e43 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.data.repository.core.support.RepositoryFactorySupport=com.azure.spring.data.cosmos.repository.support.CosmosRepositoryFactory \ No newline at end of file diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AnnotatedQueriesUserReactiveRepositoryCodeSnippet.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AnnotatedQueriesUserReactiveRepositoryCodeSnippet.java new file mode 100644 index 000000000000..201cef44c242 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AnnotatedQueriesUserReactiveRepositoryCodeSnippet.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.repository.Query; +import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.data.repository.query.Param; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +// BEGIN: readme-sample-AnnotatedQueriesUserReactiveRepositoryCodeSnippet +public interface AnnotatedQueriesUserReactiveRepositoryCodeSnippet extends ReactiveCosmosRepository { + @Query("select * from c where c.firstName = @firstName and c.lastName = @lastName") + Flux getUsersByTitleAndValue(@Param("firstName") int firstName, @Param("lastName") String lastName); + + @Query("select * from c offset @offset limit @limit") + Flux getUsersWithOffsetLimit(@Param("offset") int offset, @Param("limit") int limit); + + @Query("select count(c.id) as num_ids, c.lastName from c group by c.lastName") + Flux getCoursesGroupByDepartment(); + + @Query("select value count(1) from c where c.lastName = @lastName") + Mono getNumberOfUsersWithLastName(@Param("lastName") String lastName); +} +// END: readme-sample-AnnotatedQueriesUserReactiveRepositoryCodeSnippet diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AnnotatedQueriesUserRepositoryCodeSnippet.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AnnotatedQueriesUserRepositoryCodeSnippet.java new file mode 100644 index 000000000000..633cd0058b27 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AnnotatedQueriesUserRepositoryCodeSnippet.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import com.azure.spring.data.cosmos.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +// BEGIN: readme-sample-AnnotatedQueriesUserRepositoryCodeSnippet +public interface AnnotatedQueriesUserRepositoryCodeSnippet extends CosmosRepository { + @Query("select * from c where c.firstName = @firstName and c.lastName = @lastName") + List getUsersByFirstNameAndLastName(@Param("firstName") String firstName, @Param("lastName") String lastName); + + @Query("select * from c offset @offset limit @limit") + List getUsersWithOffsetLimit(@Param("offset") int offset, @Param("limit") int limit); + + @Query("select value count(1) from c where c.firstName = @firstName") + long getNumberOfUsersWithFirstName(@Param("firstName") String firstName); +} +// END: readme-sample-AnnotatedQueriesUserRepositoryCodeSnippet diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AppConfiguration.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AppConfiguration.java new file mode 100644 index 000000000000..1dd89b16e2f4 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AppConfiguration.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.core.credential.AzureKeyCredential; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.DirectConnectionConfig; +import com.azure.cosmos.GatewayConnectionConfig; +import com.azure.spring.data.cosmos.config.AbstractCosmosConfiguration; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.ResponseDiagnostics; +import com.azure.spring.data.cosmos.core.ResponseDiagnosticsProcessor; +import com.azure.spring.data.cosmos.repository.config.EnableCosmosRepositories; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; + +// BEGIN: readme-sample-AppConfiguration +@Configuration +@EnableCosmosRepositories +public class AppConfiguration extends AbstractCosmosConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfiguration.class); + + @Value("${azure.cosmos.uri}") + private String uri; + + @Value("${azure.cosmos.key}") + private String key; + + @Value("${azure.cosmos.secondaryKey}") + private String secondaryKey; + + @Value("${azure.cosmos.database}") + private String dbName; + + @Value("${azure.cosmos.queryMetricsEnabled}") + private boolean queryMetricsEnabled; + + @Value("${azure.cosmos.maxDegreeOfParallelism}") + private int maxDegreeOfParallelism; + + @Value("${azure.cosmos.maxBufferedItemCount}") + private int maxBufferedItemCount; + + @Value("${azure.cosmos.responseContinuationTokenLimitInKb}") + private int responseContinuationTokenLimitInKb; + + private AzureKeyCredential azureKeyCredential; + + @Bean + public CosmosClientBuilder getCosmosClientBuilder() { + this.azureKeyCredential = new AzureKeyCredential(key); + DirectConnectionConfig directConnectionConfig = new DirectConnectionConfig(); + GatewayConnectionConfig gatewayConnectionConfig = new GatewayConnectionConfig(); + return new CosmosClientBuilder() + .endpoint(uri) + .credential(azureKeyCredential) + .directMode(directConnectionConfig, gatewayConnectionConfig); + } + + @Override + public CosmosConfig cosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(queryMetricsEnabled) + .maxDegreeOfParallelism(maxDegreeOfParallelism) + .maxBufferedItemCount(maxBufferedItemCount) + .responseContinuationTokenLimitInKb(responseContinuationTokenLimitInKb) + .responseDiagnosticsProcessor(new ResponseDiagnosticsProcessorImplementation()) + .build(); + } + + public void switchToSecondaryKey() { + this.azureKeyCredential.update(secondaryKey); + } + + @Override + protected String getDatabaseName() { + return "testdb"; + } + + private static class ResponseDiagnosticsProcessorImplementation implements ResponseDiagnosticsProcessor { + + @Override + public void processResponseDiagnostics(@Nullable ResponseDiagnostics responseDiagnostics) { + LOGGER.info("Response Diagnostics {}", responseDiagnostics); + } + } + +} +// END: readme-sample-AppConfiguration diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AppConfigurationCodeSnippet.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AppConfigurationCodeSnippet.java new file mode 100644 index 000000000000..5dfb9988bd77 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AppConfigurationCodeSnippet.java @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.cosmos.DirectConnectionConfig; +import com.azure.cosmos.GatewayConnectionConfig; +import com.azure.spring.data.cosmos.config.AbstractCosmosConfiguration; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.ResponseDiagnostics; +import com.azure.spring.data.cosmos.core.ResponseDiagnosticsProcessor; +import com.azure.spring.data.cosmos.repository.config.EnableCosmosRepositories; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; + +@Configuration +@EnableCosmosRepositories +public class AppConfigurationCodeSnippet extends AbstractCosmosConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(AppConfigurationCodeSnippet.class); + + // configuration code + @Value("${azure.cosmos.uri}") + private String uri; + + @Value("${azure.cosmos.key}") + private String key; + + @Value("${azure.cosmos.secondaryKey}") + private String secondaryKey; + + @Value("${azure.cosmos.database}") + private String dbName; + + @Value("${azure.cosmos.queryMetricsEnabled}") + private boolean queryMetricsEnabled; + + @Value("${azure.cosmos.maxDegreeOfParallelism}") + private int maxDegreeOfParallelism; + + @Value("${azure.cosmos.maxBufferedItemCount}") + private int maxBufferedItemCount; + + @Value("${azure.cosmos.responseContinuationTokenLimitInKb}") + private int responseContinuationTokenLimitInKb; + + // BEGIN: readme-sample-AppConfigurationCodeSnippet + @Bean + public CosmosClientBuilder getCosmosClientBuilder() { + + DirectConnectionConfig directConnectionConfig = new DirectConnectionConfig(); + GatewayConnectionConfig gatewayConnectionConfig = new GatewayConnectionConfig(); + return new CosmosClientBuilder() + .endpoint(uri) + .directMode(directConnectionConfig, gatewayConnectionConfig); + } + + @Override + public CosmosConfig cosmosConfig() { + return CosmosConfig.builder() + .enableQueryMetrics(queryMetricsEnabled) + .maxDegreeOfParallelism(maxDegreeOfParallelism) + .maxBufferedItemCount(maxBufferedItemCount) + .responseContinuationTokenLimitInKb(responseContinuationTokenLimitInKb) + .responseDiagnosticsProcessor(new ResponseDiagnosticsProcessorImplementation()) + .build(); + } + // END: readme-sample-AppConfigurationCodeSnippet + + @Override + protected String getDatabaseName() { + return dbName; + } + + private static class ResponseDiagnosticsProcessorImplementation implements ResponseDiagnosticsProcessor { + + @Override + public void processResponseDiagnostics(@Nullable ResponseDiagnostics responseDiagnostics) { + LOGGER.info("Response Diagnostics {}", responseDiagnostics); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AuditableUser.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AuditableUser.java new file mode 100644 index 000000000000..858d4d21af08 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/AuditableUser.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; + +import java.time.OffsetDateTime; + +// BEGIN: readme-sample-AuditableUser +@Container(containerName = "myContainer") +public class AuditableUser { + private String id; + private String firstName; + @CreatedBy + private String createdBy; + @CreatedDate + private OffsetDateTime createdDate; + @LastModifiedBy + private String lastModifiedBy; + @LastModifiedDate + private OffsetDateTime lastModifiedByDate; +} +// END: readme-sample-AuditableUser diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosIndexingPolicyCodeSnippet.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosIndexingPolicyCodeSnippet.java new file mode 100644 index 000000000000..16655ed67114 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosIndexingPolicyCodeSnippet.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.cosmos.models.IndexingMode; + +public @interface CosmosIndexingPolicyCodeSnippet { + + // BEGIN: readme-sample-CosmosIndexingPolicyCodeSnippet + // Indicate if indexing policy use automatic or not + // Default value is true + boolean automatic() default Constants.DEFAULT_INDEXING_POLICY_AUTOMATIC; + + // Indexing policy mode, option Consistent. + IndexingMode mode() default IndexingMode.CONSISTENT; + + // Included paths for indexing + String[] includePaths() default {}; + + // Excluded paths for indexing + String[] excludePaths() default {}; + // END: readme-sample-CosmosIndexingPolicyCodeSnippet +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosProperties.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosProperties.java new file mode 100644 index 000000000000..cbd76815a334 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosProperties.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; +/** + * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS + * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING + * LINE NUMBERS OF EXISTING CODE SAMPLES. + */ + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "azure.cosmos") +public class CosmosProperties { + + private String uri; + + private String key; + + private String secondaryKey; + + private String database; + + private boolean queryMetricsEnabled; + + private int maxDegreeOfParallelism; + + private int maxBufferedItemCount; + + private int responseContinuationTokenLimitInKb; + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getSecondaryKey() { + return secondaryKey; + } + + public void setSecondaryKey(String secondaryKey) { + this.secondaryKey = secondaryKey; + } + + public String getDatabase() { + return database; + } + + public void setDatabase(String database) { + this.database = database; + } + + public boolean isQueryMetricsEnabled() { + return queryMetricsEnabled; + } + + public void setQueryMetricsEnabled(boolean enableQueryMetrics) { + this.queryMetricsEnabled = enableQueryMetrics; + } + + public int getMaxDegreeOfParallelism() { + return maxDegreeOfParallelism; + } + + public int getMaxBufferedItemCount() { + return maxBufferedItemCount; + } + + public int getResponseContinuationTokenLimitInKb() { + return responseContinuationTokenLimitInKb; + } + + public void setMaxDegreeOfParallelism(int maxDegreeOfParallelism) { + this.maxDegreeOfParallelism = maxDegreeOfParallelism; + } + + public void setMaxBufferedItemCount(int maxBufferedItemCount) { + this.maxBufferedItemCount = maxBufferedItemCount; + } + + public void setResponseContinuationTokenLimitInKb(int responseContinuationTokenLimitInKb) { + this.responseContinuationTokenLimitInKb = responseContinuationTokenLimitInKb; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosUniqueKeyPolicyCodeSnippet.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosUniqueKeyPolicyCodeSnippet.java new file mode 100644 index 000000000000..c2fabd2dda09 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/CosmosUniqueKeyPolicyCodeSnippet.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosUniqueKey; +import com.azure.spring.data.cosmos.core.mapping.CosmosUniqueKeyPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +// BEGIN: readme-sample-CosmosUniqueKeyPolicyCodeSnippet +@Container +@CosmosUniqueKeyPolicy(uniqueKeys = { + @CosmosUniqueKey(paths = {"/lastName", "/zipCode"}), + @CosmosUniqueKey(paths = {"/city"}) +}) +public class CosmosUniqueKeyPolicyCodeSnippet { + + @Id + String id; + + @PartitionKey + String firstName; + + String lastName; + String zipCode; + String city; +} +// END: readme-sample-CosmosUniqueKeyPolicyCodeSnippet diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/GeneratedIdEntity.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/GeneratedIdEntity.java new file mode 100644 index 000000000000..c001e1f150fe --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/GeneratedIdEntity.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.mapping.GeneratedValue; +import org.springframework.data.annotation.Id; + +// BEGIN: readme-sample-GeneratedIdEntity +public class GeneratedIdEntity { + + @Id + @GeneratedValue + private String id; + +} +// END: readme-sample-GeneratedIdEntity diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/MultiTenantDBCosmosFactory.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/MultiTenantDBCosmosFactory.java new file mode 100644 index 000000000000..fe951fff0b51 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/MultiTenantDBCosmosFactory.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos; + +import com.azure.cosmos.CosmosAsyncClient; + +/** + * Example for extending CosmosFactory for Mutli-Tenancy at the database level + */ +// BEGIN: readme-sample-MultiTenantDBCosmosFactory +public class MultiTenantDBCosmosFactory extends CosmosFactory { + + private String tenantId; + + /** + * Validate config and initialization + * + * @param cosmosAsyncClient cosmosAsyncClient + * @param databaseName databaseName + */ + public MultiTenantDBCosmosFactory(CosmosAsyncClient cosmosAsyncClient, String databaseName) { + super(cosmosAsyncClient, databaseName); + + this.tenantId = databaseName; + } + + @Override + public String getDatabaseName() { + return this.getCosmosAsyncClient().getDatabase(this.tenantId).toString(); + } +} +// END: readme-sample-MultiTenantDBCosmosFactory diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/MyItem.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/MyItem.java new file mode 100644 index 000000000000..3a6c59f54b82 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/MyItem.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.Version; + +// BEGIN: readme-sample-MyItem +@Container(containerName = "myContainer") +public class MyItem { + String id; + String data; + @Version + String _etag; +} +// END: readme-sample-MyItem diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/NestedEntitySample.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/NestedEntitySample.java new file mode 100644 index 000000000000..19714054269d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/NestedEntitySample.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +// BEGIN: readme-sample-NestedEntitySample +public class NestedEntitySample { + private String nestedPartitionKey; +} +// END: readme-sample-NestedEntitySample diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/NestedPartitionKeyEntitySample.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/NestedPartitionKeyEntitySample.java new file mode 100644 index 000000000000..77e7f3c84d90 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/NestedPartitionKeyEntitySample.java @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.mapping.Container; + +// BEGIN: readme-sample-NestedPartitionKeyEntitySample +@Container(containerName = "nested-partition-key", partitionKeyPath = "/nestedEntitySample/nestedPartitionKey") +public class NestedPartitionKeyEntitySample { + + private NestedEntitySample nestedEntitySample; +} +// END: readme-sample-NestedPartitionKeyEntitySample diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/ObjectMapperConfigurationCodeSnippet.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/ObjectMapperConfigurationCodeSnippet.java new file mode 100644 index 000000000000..2a5d18fb5c04 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/ObjectMapperConfigurationCodeSnippet.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ObjectMapperConfigurationCodeSnippet { + + // BEGIN: readme-sample-objectMapper + @Bean(name = "cosmosObjectMapper") + public ObjectMapper objectMapper() { + return new ObjectMapper(); // Do configuration to the ObjectMapper if required + } + // END: readme-sample-objectMapper + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/PageableRepositoryCodeSnippet.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/PageableRepositoryCodeSnippet.java new file mode 100644 index 000000000000..37fd579167c4 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/PageableRepositoryCodeSnippet.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.PagingAndSortingRepository; + +import java.util.List; + +public class PageableRepositoryCodeSnippet { + + @Autowired + private PagingAndSortingRepository repository; + + // BEGIN: readme-sample-findAllWithPageSize + private List findAllWithPageSize(int pageSize) { + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, pageSize, null); + Page page = repository.findAll(pageRequest); + List pageContent = page.getContent(); + while (page.hasNext()) { + Pageable nextPageable = page.nextPageable(); + page = repository.findAll(nextPageable); + pageContent = page.getContent(); + } + return pageContent; + } + // END: readme-sample-findAllWithPageSize +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SampleApplication.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SampleApplication.java new file mode 100644 index 000000000000..56be71336351 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SampleApplication.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; + +// BEGIN: readme-sample-SampleApplication +@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) { + + 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()); + + // Switch to secondary key + UserRepositoryConfiguration bean = + applicationContext.getBean(UserRepositoryConfiguration.class); + bean.switchToSecondaryKey(); + + // Now repository will use secondary key + repository.save(testUser); + + } +} +// END: readme-sample-SampleApplication diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SliceQueriesUserRepository.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SliceQueriesUserRepository.java new file mode 100644 index 000000000000..43bc506092f0 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SliceQueriesUserRepository.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import com.azure.spring.data.cosmos.repository.Query; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.repository.query.Param; + +// BEGIN: readme-sample-SliceQueriesUserRepository +public interface SliceQueriesUserRepository extends CosmosRepository { + @Query("select * from c where c.lastName = @lastName") + Slice getUsersByLastName(@Param("lastName") String lastName, Pageable pageable); +} +// END: readme-sample-SliceQueriesUserRepository diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SliceRepositoryCodeSnippet.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SliceRepositoryCodeSnippet.java new file mode 100644 index 000000000000..2ebb21fe032a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/SliceRepositoryCodeSnippet.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.util.List; + +public class SliceRepositoryCodeSnippet { + @Autowired + private SliceQueriesUserRepository repository; + + // BEGIN: readme-sample-getUsersByLastName + private List getUsersByLastName(String lastName, int pageSize) { + + final CosmosPageRequest pageRequest = new CosmosPageRequest(0, pageSize, null); + Slice slice = repository.getUsersByLastName(lastName, pageRequest); + List content = slice.getContent(); + while (slice.hasNext()) { + Pageable nextPageable = slice.nextPageable(); + slice = repository.getUsersByLastName(lastName, nextPageable); + content.addAll(slice.getContent()); + } + return content; + } + // END: readme-sample-getUsersByLastName +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/User.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/User.java new file mode 100644 index 000000000000..7b852a1370f3 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/User.java @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; + +// BEGIN: readme-sample-User +@Container(containerName = "myContainer", ru = "400") +public class User { + private String id; + private String firstName; + + + @PartitionKey + private String lastName; + + 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); + } + + 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; + } +} +// END: readme-sample-User diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserRepository.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserRepository.java new file mode 100644 index 000000000000..eaef014ef347 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserRepository.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +// BEGIN: readme-sample-UserRepository +@Repository +public interface UserRepository extends CosmosRepository { + Iterable findByFirstName(String firstName); + long countByFirstName(String firstName); + User findOne(String id, String lastName); +} +// END: readme-sample-UserRepository diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserRepositoryConfiguration.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserRepositoryConfiguration.java new file mode 100644 index 000000000000..e9d0c6d2f71e --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserRepositoryConfiguration.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; +/** + * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS + * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING + * LINE NUMBERS OF EXISTING CODE SAMPLES. + */ + +import com.azure.core.credential.AzureKeyCredential; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.config.AbstractCosmosConfiguration; +import com.azure.spring.data.cosmos.config.CosmosConfig; +import com.azure.spring.data.cosmos.core.ResponseDiagnostics; +import com.azure.spring.data.cosmos.core.ResponseDiagnosticsProcessor; +import com.azure.spring.data.cosmos.repository.config.EnableReactiveCosmosRepositories; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.lang.Nullable; + +@Configuration +@EnableConfigurationProperties(CosmosProperties.class) +@EnableReactiveCosmosRepositories +@PropertySource("classpath:application.properties") +public class UserRepositoryConfiguration extends AbstractCosmosConfiguration { + private static final Logger LOGGER = LoggerFactory.getLogger(UserRepositoryConfiguration.class); + + @Autowired + private CosmosProperties properties; + + private AzureKeyCredential azureKeyCredential; + + @Bean + public CosmosClientBuilder cosmosClientBuilder() { + this.azureKeyCredential = new AzureKeyCredential(properties.getKey()); + return new CosmosClientBuilder().credential(azureKeyCredential); + } + + @Bean + public CosmosConfig cosmosConfig() { + return CosmosConfig.builder() + .responseDiagnosticsProcessor(new ResponseDiagnosticsProcessorImplementation()) + .enableQueryMetrics(properties.isQueryMetricsEnabled()) + .maxDegreeOfParallelism(properties.getMaxDegreeOfParallelism()) + .maxBufferedItemCount(properties.getMaxBufferedItemCount()) + .responseContinuationTokenLimitInKb(properties.getResponseContinuationTokenLimitInKb()) + .build(); + } + + public void switchToSecondaryKey() { + this.azureKeyCredential.update(properties.getSecondaryKey()); + } + + public void switchToPrimaryKey() { + this.azureKeyCredential.update(properties.getKey()); + } + + public void switchKey(String key) { + this.azureKeyCredential.update(key); + } + + @Override + protected String getDatabaseName() { + return "testdb"; + } + + private static class ResponseDiagnosticsProcessorImplementation implements ResponseDiagnosticsProcessor { + + @Override + public void processResponseDiagnostics(@Nullable ResponseDiagnostics responseDiagnostics) { + LOGGER.info("Response Diagnostics {}", responseDiagnostics); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserSample.java b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserSample.java new file mode 100644 index 000000000000..05c655d770e7 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/samples/java/com/azure/spring/data/cosmos/UserSample.java @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.Id; + +// BEGIN: readme-sample-UserSample +@Container(containerName = "myContainer", autoScale = true, ru = "4000") +public class UserSample { + @Id + private String emailAddress; + +} +// END: readme-sample-UserSample diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/ExpressionResolverUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/ExpressionResolverUnitTest.java new file mode 100644 index 000000000000..40bf96bfa513 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/ExpressionResolverUnitTest.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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; + +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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/MemoizerUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/MemoizerUnitTest.java new file mode 100644 index 000000000000..2812fb2ef9c0 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/MemoizerUnitTest.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.common; + +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; +import org.junit.Before; +import org.junit.Test; + +public class MemoizerUnitTest { + + private static final String KEY = "key_1"; + private static final Map COUNT_MAP = new HashMap<>(); + private static final Function MEMOIZED_FUNCTION = + Memoizer.memoize(MemoizerUnitTest::incrCount); + + @Before + public void setUp() { + COUNT_MAP.put(KEY, new AtomicInteger(0)); + } + + @Test + public void testMemoizedFunctionShouldBeCalledOnlyOnce() { + IntStream + .range(0, 10) + .forEach(number -> MEMOIZED_FUNCTION.apply(KEY)); + + assertEquals(1, COUNT_MAP.get(KEY).get()); + } + + @Test + public void testDifferentMemoizersShouldNotShareTheSameCache() { + IntStream + .range(0, 10) + .forEach(number -> Memoizer.memoize(MemoizerUnitTest::incrCount).apply(KEY)); + + assertEquals(10, COUNT_MAP.get(KEY).get()); + } + + private static int incrCount(String key) { + return COUNT_MAP.get(key).incrementAndGet(); + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/PropertyLoaderUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/PropertyLoaderUnitTest.java new file mode 100644 index 000000000000..88c243d41bbc --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/PropertyLoaderUnitTest.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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, ""); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java new file mode 100644 index 000000000000..9f0dc6a8bf95 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/common/TestConstants.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.common; + +import com.azure.cosmos.models.IndexingMode; +import com.azure.spring.data.cosmos.domain.Address; + +import java.util.Arrays; +import java.util.List; + +public final class TestConstants { + + 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 boolean DEFAULT_INDEXING_POLICY_AUTOMATIC = true; + public static final IndexingMode DEFAULT_INDEXING_POLICY_MODE = IndexingMode.CONSISTENT; + + public static final String ROLE_COLLECTION_NAME = "RoleCollectionName"; + public static final int TIME_TO_LIVE = 5; + public static final boolean INDEXING_POLICY_AUTOMATIC = false; + public static final IndexingMode INDEXING_POLICY_MODE = IndexingMode.CONSISTENT; + + public static final String DEFAULT_UNIQUE_KEY_NAME = "/name"; + public static final String DEFAULT_UNIQUE_KEY_LEVEL = "/level"; + + 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 UPDATED_FIRST_NAME = "updated_first_name"; + public static final String UPDATED_LAST_NAME = "updated_last_name"; + + public static final String DATE_STRING = "8/8/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 CITY = "testCity"; + public static final String UPDATED_CITY = "updatedCityOne"; + public static final String STREET = "testStreet"; + public static final String UPDATED_STREET = "updatedTestStreet"; + public static final String MESSAGE = "test pojo with date"; + + public static final String CRITERIA_KEY = "CriteriaTestKey"; + public static final String CRITERIA_OBJECT = "CriteriaTestObject"; + + 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_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 String PROPERTY_ETAG_DEFAULT = "_etag"; + public static final String PROPERTY_ETAG_RENAMED = "etag"; + + public static final String DYNAMIC_PROPERTY_COLLECTION_NAME = "spel-property-collection"; + + private TestConstants() { + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosFactoryUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosFactoryUnitTest.java new file mode 100644 index 000000000000..cca2296ee87d --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosFactoryUnitTest.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.Constants; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.common.PropertyLoader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class CosmosFactoryUnitTest { + + @Test + public void userAgentSpringDataCosmosSuffix() { + CosmosClientBuilder cosmosClientBuilder = new CosmosClientBuilder(); + try { + Method updateCosmosClientBuilderWithUASuffix = CosmosFactory.class.getDeclaredMethod( + "updateCosmosClientBuilderWithUASuffix", CosmosClientBuilder.class); + + // static method invoke call + updateCosmosClientBuilderWithUASuffix.setAccessible(true); + updateCosmosClientBuilderWithUASuffix.invoke(null, cosmosClientBuilder); + + // getUserAgentSuffix method from CosmosClientBuilder + Method getUserAgentSuffix = CosmosClientBuilder.class.getDeclaredMethod("getUserAgentSuffix"); + getUserAgentSuffix.setAccessible(true); + String userAgentSuffix = (String) getUserAgentSuffix.invoke(cosmosClientBuilder); + assertThat(userAgentSuffix).contains(Constants.USER_AGENT_SUFFIX); + assertThat(userAgentSuffix).contains(PropertyLoader.getProjectVersion()); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + e.printStackTrace(); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIllegalTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIllegalTest.java new file mode 100644 index 000000000000..dd134f6c6ac7 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateIllegalTest.java @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.models.PartitionKey; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import com.azure.spring.data.cosmos.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.data.repository.query.parser.Part; +import org.springframework.util.Assert; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +@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", + CosmosQuery.class, Class.class, String.class); + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, + "faker", Arrays.asList("faker-value"), Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(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 method type + * @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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateUnitTest.java new file mode 100644 index 000000000000..54f515a8a62c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/CosmosTemplateUnitTest.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core; + +import com.azure.cosmos.CosmosAsyncClient; +import com.azure.cosmos.CosmosClientBuilder; +import com.azure.spring.data.cosmos.CosmosFactory; +import com.azure.spring.data.cosmos.common.TestConstants; +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() { + CosmosAsyncClient client = CosmosFactory.createCosmosAsyncClient(new CosmosClientBuilder() + .endpoint("") + .key("")); + new CosmosFactory(client, TestConstants.DB_NAME); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/convert/ZonedDateTimeDeserializerTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/convert/ZonedDateTimeDeserializerTest.java new file mode 100644 index 000000000000..88b593f2fe95 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/convert/ZonedDateTimeDeserializerTest.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.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.Assertions.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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/converter/MappingCosmosConverterUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/converter/MappingCosmosConverterUnitTest.java new file mode 100644 index 000000000000..1724e74a590c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/converter/MappingCosmosConverterUnitTest.java @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.converter; + +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; +import com.azure.spring.data.cosmos.core.convert.ObjectMapperFactory; +import com.azure.spring.data.cosmos.core.mapping.CosmosMappingContext; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.domain.Importance; +import com.azure.spring.data.cosmos.domain.Memo; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.domain.PersonWithEtag; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +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 java.util.UUID; + +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 JsonNode jsonNode = mappingCosmosConverter.writeJsonNode(testAddress); + + assertThat(jsonNode.get("id").asText()).isEqualTo(testAddress.getPostalCode()); + assertThat(jsonNode.get(TestConstants.PROPERTY_CITY).asText()).isEqualTo(testAddress.getCity()); + assertThat(jsonNode.get(TestConstants.PROPERTY_STREET).asText()).isEqualTo(testAddress.getStreet()); + } + + @Test + public void convertDocumentToAddressCorrectly() { + final ObjectNode objectNode = ObjectMapperFactory.getObjectMapper().createObjectNode(); + objectNode.put(TestConstants.PROPERTY_CITY, TestConstants.CITY); + objectNode.put(TestConstants.PROPERTY_STREET, TestConstants.STREET); + objectNode.put(TestConstants.PROPERTY_ID, TestConstants.POSTAL_CODE); + + final Address address = mappingCosmosConverter.read(Address.class, objectNode); + + 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 JsonNode jsonNode = mappingCosmosConverter.writeJsonNode(memo); + + assertThat(jsonNode.get(TestConstants.PROPERTY_ID).asText()).isEqualTo(memo.getId()); + assertThat(jsonNode.get(TestConstants.PROPERTY_MESSAGE).asText()).isEqualTo(memo.getMessage()); + assertThat(jsonNode.get(TestConstants.PROPERTY_DATE).asLong()).isEqualTo(memo.getDate().getTime()); + } + + @Test + public void canReadPojoWithDateFromDocument() throws ParseException { + final ObjectNode jsonObject = ObjectMapperFactory.getObjectMapper().createObjectNode(); + jsonObject.put(TestConstants.PROPERTY_MESSAGE, TestConstants.MESSAGE); + + final long date = DATE.parse(TestConstants.DATE_STRING).getTime(); + jsonObject.put(TestConstants.PROPERTY_DATE, date); + jsonObject.put(TestConstants.PROPERTY_ID, TestConstants.ID_1); + + final Memo memo = mappingCosmosConverter.read(Memo.class, jsonObject); + assertThat(memo.getId()).isEqualTo(TestConstants.ID_1); + assertThat(memo.getMessage()).isEqualTo(TestConstants.MESSAGE); + assertThat(memo.getDate().getTime()).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); + } + + @Test + public void mapsVersionFieldToEtagAndRemovesVersionField() { + final PersonWithEtag person = new PersonWithEtag(TestConstants.ID_1, TestConstants.FIRST_NAME, + TestConstants.LAST_NAME, TestConstants.HOBBIES, TestConstants.ADDRESSES); + + final String etagValue = UUID.randomUUID().toString(); + person.setEtag(etagValue); + + final JsonNode jsonNode = mappingCosmosConverter.writeJsonNode(person); + + assertThat(jsonNode.get(TestConstants.PROPERTY_ETAG_RENAMED)).isNull(); + assertThat(jsonNode.get(TestConstants.PROPERTY_ETAG_DEFAULT).asText()).isEqualTo(etagValue); + } + + @Test + public void mapsEtagOntoVersionField() { + final String etagValue = UUID.randomUUID().toString(); + final ObjectNode objectNode = ObjectMapperFactory.getObjectMapper().createObjectNode(); + objectNode.put(TestConstants.PROPERTY_ID, TestConstants.ID_1); + objectNode.put(TestConstants.PROPERTY_FIRST_NAME, TestConstants.FIRST_NAME); + objectNode.put(TestConstants.PROPERTY_LAST_NAME, TestConstants.LAST_NAME); + objectNode.put(TestConstants.PROPERTY_ETAG_DEFAULT, etagValue); + + final PersonWithEtag person = mappingCosmosConverter.read(PersonWithEtag.class, objectNode); + + assertThat(person.getEtag()).isEqualTo(etagValue); + } + + @Test + public void mapsDefaultEtag() { + final String etagValue = UUID.randomUUID().toString(); + final ObjectNode objectNode = ObjectMapperFactory.getObjectMapper().createObjectNode(); + objectNode.put(TestConstants.PROPERTY_ID, TestConstants.ID_1); + objectNode.put(TestConstants.PROPERTY_FIRST_NAME, TestConstants.FIRST_NAME); + objectNode.put(TestConstants.PROPERTY_LAST_NAME, TestConstants.LAST_NAME); + objectNode.put(TestConstants.PROPERTY_ETAG_DEFAULT, etagValue); + + final Person person = mappingCosmosConverter.read(Person.class, objectNode); + + assertThat(person.get_etag()).isEqualTo(etagValue); + + final JsonNode jsonNode = mappingCosmosConverter.writeJsonNode(person); + + assertThat(jsonNode.get(TestConstants.PROPERTY_ETAG_DEFAULT).asText()).isEqualTo(etagValue); + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/generator/AbstractQueryGeneratorTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/generator/AbstractQueryGeneratorTest.java new file mode 100644 index 000000000000..55c21c65ce11 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/generator/AbstractQueryGeneratorTest.java @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.generator; + +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.core.query.Criteria; +import com.azure.spring.data.cosmos.core.query.CriteriaType; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.parser.Part; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.springframework.data.domain.Sort.Direction.ASC; + +public class AbstractQueryGeneratorTest { + + @Mock + private EmptyQueryGenerator queryGenerator; + + @Before + public void setUp() { + this.queryGenerator = new EmptyQueryGenerator(); + } + + @Test + public void binaryOperatorPriorityPreserved() { + Criteria x = Criteria.getInstance(CriteriaType.IS_EQUAL, "x", Collections.singletonList("xVal"), Part.IgnoreCaseType.NEVER); + Criteria left = Criteria.getInstance(CriteriaType.IS_EQUAL, "y", Collections.singletonList("yVal"), Part.IgnoreCaseType.NEVER); + Criteria right = Criteria.getInstance(CriteriaType.IS_EQUAL, "z", Collections.singletonList("zVal"), Part.IgnoreCaseType.NEVER); + + Criteria or = Criteria.getInstance(CriteriaType.OR, left, right); + Criteria and = Criteria.getInstance(CriteriaType.AND, x, or); + + final CosmosQuery query = new CosmosQuery(and); + SqlQuerySpec querySpec = queryGenerator.generateCosmos(query); + List parameterNames = querySpec.getParameters().stream().map(SqlParameter::getName).collect(Collectors.toList()); + Assert.assertNotNull(querySpec.getQueryText()); + MatcherAssert.assertThat(querySpec.getQueryText(), Matchers.stringContainsInOrder( + parameterNames.get(0), CriteriaType.AND.getSqlKeyword(), + "(", parameterNames.get(1), CriteriaType.OR.getSqlKeyword(), parameterNames.get(2), ")")); + } + + @Test + public void generateBinaryQueryWithStartsWithDoesNotUseUpper() { + Criteria nameStartsWith = Criteria.getInstance(CriteriaType.STARTS_WITH, "firstName", + Collections.singletonList("TREVOR"), + Part.IgnoreCaseType.ALWAYS); + CosmosQuery query = new CosmosQuery(nameStartsWith); + + SqlQuerySpec result = queryGenerator.generateCosmos(query); + + Assert.assertEquals(result.getQueryText(), " WHERE STARTSWITH(r.firstName, @firstName0, true) "); + } + + @Test + public void generateBinaryQueryWithSort() { + Criteria hasLastName = Criteria.getInstance(CriteriaType.ARRAY_CONTAINS, "lastName", + Collections.singletonList("ANDERSON"), + Part.IgnoreCaseType.ALWAYS); + CosmosQuery query = new CosmosQuery(hasLastName).with(Sort.by(ASC, "id")); + + SqlQuerySpec result = queryGenerator.generateCosmos(query); + + Assert.assertEquals(result.getQueryText(), " WHERE ARRAY_CONTAINS(UPPER(r.lastName), UPPER(@lastName0)) ORDER BY r.id ASC"); + } + + @Test + public void generateBinaryQueryWithArrayContainsUsesUpper() { + Criteria hasLastName = Criteria.getInstance(CriteriaType.ARRAY_CONTAINS, "lastName", + Collections.singletonList("ANDERSON"), + Part.IgnoreCaseType.ALWAYS); + CosmosQuery query = new CosmosQuery(hasLastName); + + SqlQuerySpec result = queryGenerator.generateCosmos(query); + + Assert.assertEquals(result.getQueryText(), " WHERE ARRAY_CONTAINS(UPPER(r.lastName), UPPER(@lastName0)) "); + } + + @Test + public void generateBinaryQueryWithStringEquals() { + for (Part.IgnoreCaseType ignoreCaseType : Part.IgnoreCaseType.values()) { + Criteria nameStartsWith = Criteria.getInstance(CriteriaType.STRING_EQUALS, "firstName", + Collections.singletonList("TREVOR"), + ignoreCaseType); + CosmosQuery query = new CosmosQuery(nameStartsWith); + + SqlQuerySpec result = queryGenerator.generateCosmos(query); + + if (ignoreCaseType == Part.IgnoreCaseType.NEVER) { + Assert.assertEquals(result.getQueryText(), " WHERE STRINGEQUALS(r.firstName, @firstName0) "); + } else { + Assert.assertEquals(result.getQueryText(), " WHERE STRINGEQUALS(r.firstName, @firstName0, true) "); + } + } + } + + @Test + public void generateBinaryQueryWithIsEqualIntUsesUpper() { + Criteria isEqualInt = Criteria.getInstance(CriteriaType.IS_EQUAL, "zipcode", + Collections.singletonList(20180), + Part.IgnoreCaseType.ALWAYS); + CosmosQuery query = new CosmosQuery(isEqualInt); + + SqlQuerySpec result = queryGenerator.generateCosmos(query); + + Assert.assertEquals(result.getQueryText(), " WHERE UPPER(r.zipcode) = UPPER(@zipcode0) "); + } + + @Test + public void generateBinaryQueryWithIsEqualStringDoesNotUseUpper() { + Criteria isEqualString = Criteria.getInstance(CriteriaType.IS_EQUAL, "firstName", + Collections.singletonList("TREVOR"), + Part.IgnoreCaseType.ALWAYS); + CosmosQuery query = new CosmosQuery(isEqualString); + + SqlQuerySpec result = queryGenerator.generateCosmos(query); + + Assert.assertEquals(result.getQueryText(), " WHERE STRINGEQUALS(r.firstName, @firstName0, true) "); + } + + private static class EmptyQueryGenerator extends AbstractQueryGenerator implements QuerySpecGenerator { + + @Override + public SqlQuerySpec generateCosmos(CosmosQuery query) { + return this.generateCosmosQuery(query, ""); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/generator/NativeQueryGeneratorTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/generator/NativeQueryGeneratorTest.java new file mode 100644 index 000000000000..a21f4aeb60fc --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/generator/NativeQueryGeneratorTest.java @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.generator; + +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.springframework.data.domain.Sort; + +import java.util.ArrayList; +import java.util.List; + +import static org.springframework.data.domain.Sort.Direction.ASC; + +public class NativeQueryGeneratorTest { + + @Mock + private EmptyQueryGenerator queryGenerator; + + @Before + public void setUp() { + this.queryGenerator = new EmptyQueryGenerator(); + } + + @Test + public void generateSortedQueryTest() { + List sqlParameters = new ArrayList<>(); + sqlParameters.add(new SqlParameter("@firstName", "TREVOR")); + + SqlQuerySpec querySpec = new SqlQuerySpec("select * from a where a.firstName = @firstName", sqlParameters); + final SqlQuerySpec sortedQuerySpec = NativeQueryGenerator.getInstance().generateSortedQuery(querySpec, Sort.by(ASC, "id")); + + Assert.assertEquals(sortedQuerySpec.getQueryText(), "select * from a where a.firstName = @firstName ORDER BY a.id ASC"); + } + + private static class EmptyQueryGenerator extends NativeQueryGenerator implements QuerySpecGenerator { + + @Override + public SqlQuerySpec generateCosmos(CosmosQuery query) { + return this.generateCosmos(query); + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentEntityUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentEntityUnitTest.java new file mode 100644 index 000000000000..aadb113e5acd --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/BasicCosmosPersistentEntityUnitTest.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.core.mapping; + +import com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/CosmosMappingContextUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/CosmosMappingContextUnitTest.java new file mode 100644 index 000000000000..2ed4fbbdc906 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/CosmosMappingContextUnitTest.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/CosmosEventListenerUnitTests.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/CosmosEventListenerUnitTests.java new file mode 100644 index 000000000000..f2c03a646b16 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/mapping/event/CosmosEventListenerUnitTests.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.mapping.event; + +import static org.assertj.core.api.Assertions.*; + +import com.azure.spring.data.cosmos.domain.IPerson; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.domain.Student; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; +import org.junit.Test; + +public class CosmosEventListenerUnitTests { + + @Test + public void afterLoadEffectGetsHandledCorrectly() { + SamplePersonEventListener listener = new SamplePersonEventListener(); + listener.onApplicationEvent(new AfterLoadEvent<>(NullNode.getInstance(), Person.class, "container-1")); + assertThat(listener.invokedOnAfterLoad).isTrue(); + } + + @Test + public void afterLoadEventGetsFilteredForDomainType() { + SamplePersonEventListener personListener = new SamplePersonEventListener(); + SampleStudentEventListener studentListener = new SampleStudentEventListener(); + personListener.onApplicationEvent(new AfterLoadEvent<>(NullNode.getInstance(), Person.class, "container-1")); + studentListener.onApplicationEvent(new AfterLoadEvent<>(NullNode.getInstance(), Person.class, "container-1")); + + assertThat(personListener.invokedOnAfterLoad).isTrue(); + assertThat(studentListener.invokedOnAfterLoad).isFalse(); + } + + @Test + public void afterLoadEventGetsFilteredForDomainTypeWorksForSubtypes() { + SamplePersonEventListener personListener = new SamplePersonEventListener(); + SampleIPersonEventListener contactListener = new SampleIPersonEventListener(); + personListener.onApplicationEvent(new AfterLoadEvent<>(NullNode.getInstance(), Person.class, "container-1")); + contactListener.onApplicationEvent(new AfterLoadEvent<>(NullNode.getInstance(), Person.class, "container-1")); + + assertThat(personListener.invokedOnAfterLoad).isTrue(); + assertThat(contactListener.invokedOnAfterLoad).isTrue(); + } + + @Test + public void afterLoadEventGetsFilteredForDomainTypeWorksForSubtypes2() { + SamplePersonEventListener personListener = new SamplePersonEventListener(); + SampleIPersonEventListener contactListener = new SampleIPersonEventListener(); + personListener.onApplicationEvent(new AfterLoadEvent<>(NullNode.getInstance(), IPerson.class, "container-1")); + contactListener.onApplicationEvent(new AfterLoadEvent<>(NullNode.getInstance(), IPerson.class, "container-1")); + + assertThat(personListener.invokedOnAfterLoad).isFalse(); + assertThat(contactListener.invokedOnAfterLoad).isTrue(); + } + + @Test + public void dontInvokePersonCallbackForStudentEvent() { + CosmosMappingEvent event = new AfterLoadEvent<>(NullNode.getInstance(), Student.class, "container-1"); + SamplePersonEventListener listener = new SamplePersonEventListener(); + listener.onApplicationEvent(event); + assertThat(listener.invokedOnAfterLoad).isFalse(); + } + + static class SamplePersonEventListener extends AbstractCosmosEventListener { + + boolean invokedOnAfterLoad; + + @Override + public void onAfterLoad(AfterLoadEvent event) { + invokedOnAfterLoad = true; + } + } + + static class SampleStudentEventListener extends AbstractCosmosEventListener { + + boolean invokedOnAfterLoad; + + @Override + public void onAfterLoad(AfterLoadEvent event) { + invokedOnAfterLoad = true; + } + } + + static class SampleIPersonEventListener extends AbstractCosmosEventListener { + + boolean invokedOnAfterLoad; + + @Override + public void onAfterLoad(AfterLoadEvent event) { + invokedOnAfterLoad = true; + } + } + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/query/CosmosQueryUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/query/CosmosQueryUnitTest.java new file mode 100644 index 000000000000..39e0e584129a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/query/CosmosQueryUnitTest.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.query; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.parser.Part; + +import java.util.Arrays; + +import static com.azure.spring.data.cosmos.common.TestConstants.CRITERIA_KEY; +import static com.azure.spring.data.cosmos.common.TestConstants.CRITERIA_OBJECT; + +public class CosmosQueryUnitTest { + + @Test + public void testDocumentQueryCreate() { + final Criteria criteria = Criteria.getInstance(CriteriaType.IS_EQUAL, CRITERIA_KEY, + Arrays.asList(CRITERIA_OBJECT), Part.IgnoreCaseType.NEVER); + + final CosmosQuery query = new CosmosQuery(criteria); + + Assert.assertEquals(criteria, query.getCriteria()); + Assert.assertEquals(Sort.unsorted(), query.getSort()); + Assert.assertEquals(Pageable.unpaged(), query.getPageable()); + Assert.assertEquals(Part.IgnoreCaseType.NEVER, criteria.getIgnoreCase()); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/query/CriteriaUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/query/CriteriaUnitTest.java new file mode 100644 index 000000000000..3ac5bdd4e561 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/core/query/CriteriaUnitTest.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.core.query; + +import com.azure.spring.data.cosmos.core.generator.FindQuerySpecGenerator; +import com.azure.spring.data.cosmos.exception.IllegalQueryException; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.data.repository.query.parser.Part; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.azure.spring.data.cosmos.common.TestConstants.CRITERIA_KEY; +import static com.azure.spring.data.cosmos.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, Part.IgnoreCaseType.NEVER); + + 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())); + Assert.assertEquals(Part.IgnoreCaseType.NEVER, criteria.getIgnoreCase()); + } + + @Test + public void testBinaryCriteria() { + final List values = Arrays.asList(CRITERIA_OBJECT); + final Criteria leftCriteria = Criteria.getInstance(CriteriaType.IS_EQUAL, + CRITERIA_KEY, values, Part.IgnoreCaseType.NEVER); + final Criteria rightCriteria = Criteria.getInstance(CriteriaType.IS_EQUAL, + CRITERIA_OBJECT, values, Part.IgnoreCaseType.NEVER); + 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)); + + Assert.assertEquals(Part.IgnoreCaseType.NEVER, criteria.getSubCriteria().get(0).getIgnoreCase()); + Assert.assertEquals(Part.IgnoreCaseType.NEVER, criteria.getSubCriteria().get(1).getIgnoreCase()); + } + + @Test(expected = IllegalQueryException.class) + public void testInvalidInKeywordParameter() { + final List values = Collections.singletonList(CRITERIA_OBJECT); + final Criteria criteria = Criteria.getInstance(CriteriaType.IN, + CRITERIA_KEY, values, Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(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, Part.IgnoreCaseType.NEVER); + final CosmosQuery query = new CosmosQuery(criteria); + + new FindQuerySpecGenerator().generateCosmos(query); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Address.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Address.java new file mode 100644 index 000000000000..e43343abc045 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Address.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container() +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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/IPerson.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/IPerson.java new file mode 100644 index 000000000000..79ccf1a6a99b --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/IPerson.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +public interface IPerson { + +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Importance.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Importance.java new file mode 100644 index 000000000000..258af29c1494 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Importance.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +public enum Importance { + NORMAL; +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomain.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomain.java new file mode 100644 index 000000000000..8c4021132c48 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/LongIdDomain.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@Container +public class LongIdDomain { + + @Id + private Long number; + + private String name; + + public LongIdDomain(Long number, String name) { + this.number = number; + this.name = name; + } + + public LongIdDomain() { + } + + public Long getNumber() { + return number; + } + + public void setNumber(Long 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; + } + LongIdDomain that = (LongIdDomain) 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 "LongIdDomain{" + + "number=" + + number + + ", name='" + + name + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Memo.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Memo.java new file mode 100644 index 000000000000..b46578d3402a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Memo.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; + +import java.util.Date; +import java.util.Objects; + +/** + * For testing date and enum purpose + */ +@Container() +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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/NoDBAnnotationPerson.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/NoDBAnnotationPerson.java new file mode 100644 index 000000000000..8bc73b7b3a96 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/NoDBAnnotationPerson.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.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() { + } + + 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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Person.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Person.java new file mode 100644 index 000000000000..4a519e4f1820 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Person.java @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Version; + +import java.util.List; +import java.util.Objects; + +@Container() +@CosmosIndexingPolicy() +public class Person implements IPerson { + 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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/PersonWithEtag.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/PersonWithEtag.java new file mode 100644 index 000000000000..a4fcbe0d4b16 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/PersonWithEtag.java @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Version; + +import java.util.List; +import java.util.Objects; + +@Container() +@CosmosIndexingPolicy() +public class PersonWithEtag { + private String id; + private String firstName; + + @PartitionKey + private String lastName; + private List hobbies; + private List
shippingAddresses; + @Version + private String etag; + + public PersonWithEtag(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 PersonWithEtag() { + } + + 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 getEtag() { + return etag; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PersonWithEtag person = (PersonWithEtag) 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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Role.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Role.java new file mode 100644 index 000000000000..f000895e4b68 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Role.java @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.cosmos.models.IndexingMode; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.core.mapping.CosmosUniqueKey; +import com.azure.spring.data.cosmos.core.mapping.CosmosUniqueKeyPolicy; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +@CosmosIndexingPolicy( + mode = IndexingMode.CONSISTENT, + automatic = TestConstants.INDEXING_POLICY_AUTOMATIC) +@Container(containerName = TestConstants.ROLE_COLLECTION_NAME, + autoCreateContainer = false) +@CosmosUniqueKeyPolicy(uniqueKeys = { + @CosmosUniqueKey(paths = {TestConstants.DEFAULT_UNIQUE_KEY_NAME, TestConstants.DEFAULT_UNIQUE_KEY_LEVEL}) +}) +public class Role { + @Id + String id; + + @PartitionKey + boolean developer; + + String name; + + String level; + + public Role() { + } + + public Role(String id, boolean developer, String name, String level) { + this.id = id; + this.developer = developer; + 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; + } + + public boolean isDeveloper() { + return developer; + } + + public void setDeveloper(boolean developer) { + this.developer = developer; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Role role = (Role) o; + return developer == role.developer && Objects.equals(id, role.id) + && Objects.equals(name, role.name) && Objects.equals(level, role.level); + } + + @Override + public int hashCode() { + return Objects.hash(id, developer, name, level); + } + + @Override + public String toString() { + return "Role{" + + "id='" + id + '\'' + + ", isDeveloper=" + developer + + ", name='" + name + '\'' + + ", level='" + level + '\'' + + '}'; + } +} + diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Student.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Student.java new file mode 100644 index 000000000000..3f0ae3dc54ff --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/Student.java @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; + +import java.util.Objects; + +@CosmosIndexingPolicy() +public class Student { + private String id; + private String firstName; + private String lastName; + + public Student() { + } + + 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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/TimeToLiveSample.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/TimeToLiveSample.java new file mode 100644 index 000000000000..01064077006a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/TimeToLiveSample.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.mapping.Container; + +@Container(timeToLive = TestConstants.TIME_TO_LIVE) +public class TimeToLiveSample { + private String id; + + public TimeToLiveSample() { + } + + public TimeToLiveSample(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/UUIDIdDomain.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/UUIDIdDomain.java new file mode 100644 index 000000000000..f3b9f8141445 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/domain/UUIDIdDomain.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.domain; + +import com.azure.spring.data.cosmos.core.mapping.Container; +import org.springframework.data.annotation.Id; + +import java.util.Objects; +import java.util.UUID; + +@Container +public class UUIDIdDomain { + + @Id + private UUID number; + + private String name; + + public UUIDIdDomain(UUID number, String name) { + this.number = number; + this.name = name; + } + + public UUIDIdDomain() { + } + + public UUID getNumber() { + return number; + } + + public void setNumber(UUID 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; + } + UUIDIdDomain that = (UUIDIdDomain) 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 "UUIDIdDomain{" + + "number=" + + number + + ", name='" + + name + + '\'' + + '}'; + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/CosmosAnnotationUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/CosmosAnnotationUnitTest.java new file mode 100644 index 000000000000..5f8e5e50d47a --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/CosmosAnnotationUnitTest.java @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import com.azure.cosmos.models.IndexingPolicy; +import com.azure.cosmos.models.UniqueKey; +import com.azure.cosmos.models.UniqueKeyPolicy; +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.CosmosIndexingPolicy; +import com.azure.spring.data.cosmos.domain.NoDBAnnotationPerson; +import com.azure.spring.data.cosmos.domain.Role; +import com.azure.spring.data.cosmos.domain.TimeToLiveSample; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.junit.Before; +import org.junit.Test; +import org.springframework.util.Assert; + +import java.util.List; + + +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 testDefaultUniqueKeyPolicyAnnotation() { + final UniqueKeyPolicy uniqueKeyPolicy = personInfo.getUniqueKeyPolicy(); + Assert.isNull(uniqueKeyPolicy, "NoDBAnnotationPerson class should not have CosmosUniqueKeyPolicy annotation"); + } + + @Test + public void testDefaultIndexingPolicyAnnotation() { + final IndexingPolicy policy = personInfo.getIndexingPolicy(); + final Container containerAnnotation = NoDBAnnotationPerson.class.getAnnotation(Container.class); + final CosmosIndexingPolicy policyAnnotation = + NoDBAnnotationPerson.class.getAnnotation(CosmosIndexingPolicy.class); + + Assert.isNull(containerAnnotation, "NoDBAnnotationPerson class should not have Container annotation"); + Assert.isNull(policyAnnotation, "NoDBAnnotationPerson class should not have CosmosIndexingPolicy 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(policy.isAutomatic() == TestConstants.DEFAULT_INDEXING_POLICY_AUTOMATIC, + "should be default indexing policy automatic"); + Assert.isTrue(policy.getIndexingMode() == TestConstants.DEFAULT_INDEXING_POLICY_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. + Assert.isTrue(policy.getIncludedPaths().isEmpty(), "default includedpaths size must be 0"); + Assert.isTrue(policy.getExcludedPaths().isEmpty(), "default excludedpaths size must be 0"); + } + + @Test + public void testIndexingPolicyAnnotation() { + final IndexingPolicy policy = roleInfo.getIndexingPolicy(); + final Container containerAnnotation = Role.class.getAnnotation(Container.class); + final CosmosIndexingPolicy policyAnnotation = Role.class.getAnnotation(CosmosIndexingPolicy.class); + + // ContainerName, RequestUnit, Automatic and IndexingMode + Assert.notNull(containerAnnotation, "Role class should have Container annotation"); + Assert.notNull(policyAnnotation, "Role class should have CosmosIndexingPolicy annotation"); + Assert.notNull(policy, "Role class collection policy should not be null"); + + Assert.isTrue(roleInfo.getContainerName().equals(TestConstants.ROLE_COLLECTION_NAME), + "should be Role(class) collection name"); + Assert.isTrue(policy.isAutomatic() == TestConstants.INDEXING_POLICY_AUTOMATIC, + "should be Role(class) indexing policy automatic"); + Assert.isTrue(policy.getIndexingMode() == TestConstants.INDEXING_POLICY_MODE, + "should be Role(class) indexing policy mode"); + } + + @Test + public void testUniqueKeyPolicyAnnotation() { + final UniqueKeyPolicy uniqueKeyPolicy = roleInfo.getUniqueKeyPolicy(); + Assert.notNull(uniqueKeyPolicy, "Role class should have CosmosUniqueKeyPolicy annotation"); + List uniqueKeys = uniqueKeyPolicy.getUniqueKeys(); + + Assert.notNull(uniqueKeys, "Role class should have CosmosUniqueKey annotation"); + Assert.notEmpty(uniqueKeys, "Role class should have non empty CosmosUniqueKey annotation"); + + Assert.isTrue(uniqueKeys.size() == 1, "Role class should have 1 set of unique keys"); + + UniqueKey uniqueKey = uniqueKeys.get(0); + + Assert.isTrue(uniqueKey.getPaths().size() == 2, "Role class should have 1 set of unique keys with 2 paths"); + + Assert.isTrue(uniqueKey.getPaths().contains(TestConstants.DEFAULT_UNIQUE_KEY_LEVEL), "Role class should have path /level in unique keys"); + Assert.isTrue(uniqueKey.getPaths().contains(TestConstants.DEFAULT_UNIQUE_KEY_NAME), "Role class should have path /name in unique keys"); + } + + @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 testDefaultContainerAnnotationTimeToLive() { + 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 testContainerAnnotationTimeToLive() { + 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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/SimpleCosmosRepositoryIllegalTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/SimpleCosmosRepositoryIllegalTest.java new file mode 100644 index 000000000000..053ebd265c0c --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/SimpleCosmosRepositoryIllegalTest.java @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; + +import com.azure.cosmos.models.CosmosContainerProperties; +import com.azure.cosmos.models.PartitionKeyDefinition; +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.azure.spring.data.cosmos.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() { + CosmosContainerProperties containerProperties = new CosmosContainerProperties("", new PartitionKeyDefinition()); + 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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/SimpleCosmosRepositoryUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/SimpleCosmosRepositoryUnitTest.java new file mode 100644 index 000000000000..b59dce650977 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/SimpleCosmosRepositoryUnitTest.java @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository; + +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.azure.spring.data.cosmos.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.any; +import static org.mockito.ArgumentMatchers.anyString; +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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoryConfigurationExtensionUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoryConfigurationExtensionUnitTest.java new file mode 100644 index 000000000000..73945c6eeee2 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/config/CosmosRepositoryConfigurationExtensionUnitTest.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.config; + +import com.azure.spring.data.cosmos.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.AnnotationMetadata; +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 { + + AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class); + ResourceLoader loader = new PathMatchingResourcePatternResolver(); + Environment environment = new StandardEnvironment(); + RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableCosmosRepositories.class, loader, environment, new DefaultListableBeanFactory(), null); + + 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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoryConfigurationExtensionUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoryConfigurationExtensionUnitTest.java new file mode 100644 index 000000000000..57e518e03ef6 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/config/ReactiveCosmosRepositoryConfigurationExtensionUnitTest.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.data.cosmos.repository.config; + +import com.azure.spring.data.cosmos.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.AnnotationMetadata; +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 { + + AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class); + ResourceLoader loader = new PathMatchingResourcePatternResolver(); + Environment environment = new StandardEnvironment(); + RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, + EnableReactiveCosmosRepositories.class, loader, environment, new DefaultListableBeanFactory(), null); + + 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/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQueryUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQueryUnitTest.java new file mode 100644 index 000000000000..33002512f898 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQueryUnitTest.java @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosPageRequest; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.util.Assert; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class AbstractCosmosQueryUnitTest { + + @Mock + CosmosQueryMethod method; + + @Mock + CosmosParameterAccessor accessor; + + @Test + public void testShouldUseMultiEntityExecutionIfMethodIsCollectionQuery() { + when(method.isCollectionQuery()).thenReturn(true); + TestCosmosQuery cosmosQuery = new TestCosmosQuery(method, null); + CosmosQueryExecution execution = cosmosQuery.getExecution(null, null); + Assert.isInstanceOf(CosmosQueryExecution.MultiEntityExecution.class, execution); + } + + @Test + public void testShouldUseSingleExecutionAsFinalCase() { + when(method.isCollectionQuery()).thenReturn(false); + TestCosmosQuery cosmosQuery = new TestCosmosQuery(method, null); + CosmosQueryExecution execution = cosmosQuery.getExecution(null, null); + Assert.isInstanceOf(CosmosQueryExecution.SingleEntityExecution.class, execution); + } + + @Test + public void testShouldUseSliceExecutionIfMethodIsSliceQuery() { + when(method.isSliceQuery()).thenReturn(true); + when(accessor.getPageable()).thenReturn(CosmosPageRequest.of(0, 10)); + TestCosmosQuery cosmosQuery = new TestCosmosQuery(method, null); + CosmosQueryExecution execution = cosmosQuery.getExecution(accessor, null); + Assert.isInstanceOf(CosmosQueryExecution.SliceExecution.class, execution); + } + + private class TestCosmosQuery extends AbstractCosmosQuery { + + TestCosmosQuery(CosmosQueryMethod method, CosmosOperations operations) { + super(method, operations); + } + + @Override + protected CosmosQuery createQuery(CosmosParameterAccessor accessor) { + return null; + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + protected boolean isCountQuery() { + return false; + } + + @Override + protected boolean isPageQuery() { + return false; + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQueryUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQueryUnitTest.java new file mode 100644 index 000000000000..f64a20601134 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQueryUnitTest.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.query; + +import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RunWith(MockitoJUnitRunner.class) +public class AbstractReactiveCosmosQueryUnitTest { + + @Mock + ReactiveCosmosQueryMethod method; + + @Test + public void testShouldUseMultiEntityExecutionIfMethodHasFluxReactiveWrapper() { + Mockito.>when(method.getReactiveWrapper()).thenReturn(Flux.class); + TestReactiveCosmosQuery cosmosQuery = new TestReactiveCosmosQuery(method, null); + ReactiveCosmosQueryExecution execution = cosmosQuery.getExecution(null); + Assert.isInstanceOf(ReactiveCosmosQueryExecution.MultiEntityExecution.class, execution); + } + + @Test + public void testShouldUseSingleExecutionIfMethodHasMonoReactiveWrapper() { + Mockito.>when(method.getReactiveWrapper()).thenReturn(Mono.class); + TestReactiveCosmosQuery cosmosQuery = new TestReactiveCosmosQuery(method, null); + ReactiveCosmosQueryExecution execution = cosmosQuery.getExecution(null); + Assert.isInstanceOf(ReactiveCosmosQueryExecution.SingleEntityExecution.class, execution); + } + + private class TestReactiveCosmosQuery extends AbstractReactiveCosmosQuery { + + TestReactiveCosmosQuery(ReactiveCosmosQueryMethod method, ReactiveCosmosOperations operations) { + super(method, operations); + } + + @Override + protected CosmosQuery createQuery(ReactiveCosmosParameterAccessor accessor) { + return null; + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + protected boolean isCountQuery() { + return false; + } + + @Override + protected boolean isPageQuery() { + return false; + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersonRepository.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersonRepository.java new file mode 100644 index 000000000000..e305178f6b75 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/repository/PersonRepository.java @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.repository; + +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.repository.CosmosRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PersonRepository extends CosmosRepository { +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosEntityInformationUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosEntityInformationUnitTest.java new file mode 100644 index 000000000000..f62e6f2d9999 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosEntityInformationUnitTest.java @@ -0,0 +1,512 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.spring.data.cosmos.common.TestConstants; +import com.azure.spring.data.cosmos.core.mapping.Container; +import com.azure.spring.data.cosmos.core.mapping.PartitionKey; +import com.azure.spring.data.cosmos.domain.Address; +import com.azure.spring.data.cosmos.domain.LongIdDomain; +import com.azure.spring.data.cosmos.domain.Person; +import com.azure.spring.data.cosmos.domain.Student; +import com.azure.spring.data.cosmos.domain.UUIDIdDomain; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +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("testContainer"); + } + + @Test + public void testGetPartitionKeyName() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(VolunteerWithPartitionKey.class); + + final String partitionKeyName = entityInformation.getPartitionKeyPath(); + assertThat(partitionKeyName).isEqualTo("/name"); + } + + @Test + public void testNullPartitionKeyName() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(Volunteer.class); + + final String partitionKeyName = entityInformation.getPartitionKeyPath(); + assertThat(partitionKeyName).isEqualTo("/null"); + } + + @Test + public void testCustomPartitionKeyName() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(VolunteerWithCustomPartitionKey.class); + + final String partitionKeyName = entityInformation.getPartitionKeyPath(); + assertThat(partitionKeyName).isEqualTo("/vol_name"); + } + + @Test + public void testPartitionKeyPathAnnotation() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(VolunteerWithPartitionKeyPath.class); + + final String partitionKeyPath = entityInformation.getPartitionKeyPath(); + assertThat(partitionKeyPath).isEqualTo("/partitionKeyPath"); + } + + @Test + public void testPartitionKeyPathAndPartitionKeyAnnotation() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(VolunteerWithPartitionKeyPathAndPartitionKey.class); + + final String partitionKeyPath = entityInformation.getPartitionKeyPath(); + assertThat(partitionKeyPath).isEqualTo("/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 testEntityShouldBeVersionedIfUsingAnnotationOnAStringField() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(VersionFieldDifferentName.class); + final boolean isVersioned = entityInformation.isVersioned(); + assertThat(isVersioned).isTrue(); + } + + @Test + public void testNonVersionedEntity() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation(Student.class); + + final boolean isVersioned = entityInformation.isVersioned(); + assertThat(isVersioned).isFalse(); + } + + @Container(containerName = "testContainer") + private static class Volunteer { + String id; + String name; + } + + @Container + 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; + } + } + + @Container + 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; + } + } + + @Container(partitionKeyPath = "/partitionKeyPath") + private static class VolunteerWithPartitionKeyPath { + private String id; + private String name; + } + + @Container(partitionKeyPath = "/partitionKeyPath") + private static class VolunteerWithPartitionKeyPathAndPartitionKey { + private String id; + @PartitionKey + private String name; + } + + @Container(containerName = "testContainer") + 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 + + '\'' + + '}'; + } + } + + @Container + 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 + + '}'; + } + } + + @Container + private static class VersionFieldDifferentName { + private String id; + private String name; + @Version + private String version; + + VersionFieldDifferentName() { + } + + 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 getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VersionFieldDifferentName that = (VersionFieldDifferentName) o; + return Objects.equals(id, that.id) + && Objects.equals(name, that.name) + && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, version); + } + + @Override + public String toString() { + return "VersionFieldDifferentName{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", version='" + + version + + '\'' + + '}'; + } + } + + @Test + public void testGetIdFieldWithUUIDType() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(UUIDIdDomain.class); + assertThat(entityInformation.getIdField().getType().equals(UUID.class)).isTrue(); + } + + @Test + public void testUUIDIdTypeSerialization() { + final UUID uuid = UUID.randomUUID(); + String s = uuid.toString(); + assertThat(uuid).isEqualTo(UUID.fromString(s)); + String lowerCase = s.toLowerCase(); + String upperCase = s.toUpperCase(); + assertThat(uuid).isEqualTo(UUID.fromString(lowerCase)); + assertThat(uuid).isEqualTo(UUID.fromString(upperCase)); + assertThat(UUID.fromString(upperCase).toString()).isEqualTo(UUID.fromString(lowerCase).toString()); + } + + @Test + public void testGetIdFieldWithLongType() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(LongIdDomain.class); + assertThat(entityInformation.getIdField().getType().equals(Long.class)).isTrue(); + } + + @Test + public void testGetIdFieldWithBasicType() { + final CosmosEntityInformation entityInformation = + new CosmosEntityInformation<>(BasicLongIdDomain.class); + assertThat(entityInformation.getIdField().getType().equals(long.class)).isTrue(); + } + + @Container + class BasicLongIdDomain { + + @Id + private long number; + + private String name; + + BasicLongIdDomain(long number, String name) { + this.number = number; + this.name = name; + } + + BasicLongIdDomain() { + } + + public long getNumber() { + return number; + } + + public void setNumber(Long 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; + } + BasicLongIdDomain that = (BasicLongIdDomain) 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 "BasicLongIdDomain{" + + "number=" + + number + + ", name='" + + name + + '\'' + + '}'; + } + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryBeanUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryBeanUnitTest.java new file mode 100644 index 000000000000..01d75b321d39 --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryBeanUnitTest.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.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; + +@RunWith(MockitoJUnitRunner.class) +public class CosmosRepositoryFactoryBeanUnitTest { + + @Mock + CosmosTemplate template; + + @Test + public void testCreateRepositoryFactory() { + final CosmosRepositoryFactoryBean factoryBean = + new CosmosRepositoryFactoryBean<>(PersonRepository.class); + factoryBean.setCosmosOperations(template); + final RepositoryFactorySupport factory = factoryBean.createRepositoryFactory(); + assertThat(factory).isNotNull(); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryUnitTest.java b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryUnitTest.java new file mode 100644 index 000000000000..4b58b716e4df --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactoryUnitTest.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.spring.data.cosmos.core.CosmosTemplate; +import com.azure.spring.data.cosmos.domain.Person; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.repository.core.EntityInformation; + +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class CosmosRepositoryFactoryUnitTest { + + @Mock + CosmosTemplate cosmosTemplate; + + @Test + public void useMappingCosmosDBEntityInfoIfMappingContextSet() { + final CosmosRepositoryFactory factory = new CosmosRepositoryFactory(cosmosTemplate); + final EntityInformation entityInfo = factory.getEntityInformation(Person.class); + assertTrue(entityInfo instanceof CosmosEntityInformation); + } +} diff --git a/sdk/spring/azure-spring-data-cosmos/src/test/resources/application.properties b/sdk/spring/azure-spring-data-cosmos/src/test/resources/application.properties new file mode 100644 index 000000000000..77f6fb59513f --- /dev/null +++ b/sdk/spring/azure-spring-data-cosmos/src/test/resources/application.properties @@ -0,0 +1,28 @@ +cosmos.uri=${COSMOS_SPRING_ACCOUNT_HOST} +cosmos.key=${COSMOS_SPRING_ACCOUNT_KEY} +cosmos.secondaryKey=${COSMOS_SPRING_SECONDARY_ACCOUNT_KEY} + +dynamic.collection.name=spel-property-collection +# Populate query metrics +cosmos.queryMetricsEnabled=true +# Max Degree of Parallelism allowed +cosmos.maxDegreeOfParallelism=0 +# Max number of items to buffer +cosmos.maxBufferedItemCount=0 +# Max size of the response continuation token +cosmos.responseContinuationTokenLimitInKb=0 + +# Secondary DataSource Config +cosmos.secondary.uri=${COSMOS_SPRING_NEW_ACCOUNT_HOST} +cosmos.secondary.key=${COSMOS_SPRING_NEW_ACCOUNT_KEY} +cosmos.secondary.secondaryKey=${COSMOS_SPRING_NEW_SECONDARY_ACCOUNT_KEY} + +# Populate query metrics +cosmos.secondary.queryMetricsEnabled=true +# Max Degree of Parallelism allowed +cosmos.secondary.maxDegreeOfParallelism=0 +# Max number of items to buffer +cosmos.secondary.maxBufferedItemCount=0 +# Max size of the response continuation token +cosoms.secondary.responseContinuationTokenLimitInKb=0 + diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml index 94043e2638e7..d71a77ac170b 100644 --- a/sdk/spring/ci.yml +++ b/sdk/spring/ci.yml @@ -27,6 +27,10 @@ pr: # boolean for each library. Note: AdditionalModules are never to have # parameter entries as those are not releasing libraries. parameters: +- name: release_azurespringdatacosmos + displayName: 'azure-spring-data-cosmos' + type: boolean + default: true - name: release_springcloudazurecore displayName: 'spring-cloud-azure-core' type: boolean @@ -223,6 +227,10 @@ extends: ServiceDirectory: spring EnableBatchRelease: true Artifacts: + - name: azure-spring-data-cosmos + groupId: com.azure + safeName: azurespringdatacosmos + releaseInBatch: ${{ parameters.release_azurespringdatacosmos }} - name: spring-cloud-azure-core groupId: com.azure.spring safeName: springcloudazurecore @@ -543,5 +551,8 @@ extends: groupId: com.azure.spring safeName: springcloudazurestarterappconfigurationconfig releaseInBatch: ${{ parameters.release_springcloudazurestarterappconfigurationconfig }} + AdditionalModules: + - name: azure-spring-data-cosmos-test + groupId: com.azure MatrixFilters: - JavaTestVersion=^(?!1.8|1.11).* diff --git a/sdk/spring/cosmos-integration-matrix.json b/sdk/spring/cosmos-integration-matrix.json new file mode 100644 index 000000000000..da3fd9208ee1 --- /dev/null +++ b/sdk/spring/cosmos-integration-matrix.json @@ -0,0 +1,13 @@ +{ + "matrix": { + "Cosmos": { + "Session_Integration": { + "ArmTemplateParameters": "@{ enableMultipleWriteLocations = $false; defaultConsistencyLevel = 'Session' }", + "ProfileFlag": "-P integration-test-azure", + "Pool": "azsdk-pool-mms-ubuntu-2004-general", + "OSVmImage": "MMSUbuntu2004" + } + }, + "JavaTestVersion": ["1.8", "1.11", "1.17"] + } +} diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml index f5264332c6b6..388ec7c4c22e 100644 --- a/sdk/spring/pom.xml +++ b/sdk/spring/pom.xml @@ -66,6 +66,8 @@ spring-cloud-azure-feature-management spring-cloud-azure-feature-management-web spring-cloud-azure-starter-appconfiguration-config + azure-spring-data-cosmos + azure-spring-data-cosmos-test @@ -124,6 +126,8 @@ spring-cloud-azure-feature-management spring-cloud-azure-feature-management-web spring-cloud-azure-starter-appconfiguration-config + azure-spring-data-cosmos + azure-spring-data-cosmos-test diff --git a/sdk/spring/spring-cloud-azure-integration-tests/test-resources/cosmos-spring/test-resources.json b/sdk/spring/spring-cloud-azure-integration-tests/test-resources/cosmos-spring/test-resources.json new file mode 100644 index 000000000000..7cdd90f6ec52 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-integration-tests/test-resources/cosmos-spring/test-resources.json @@ -0,0 +1,146 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "baseName": { + "type": "string" + }, + "enableMultipleWriteLocations" : { + "type": "bool", + "metadata": { + "description": "Flag to enable or disable multiple write locations on CosmosDB Account" + } + }, + "defaultConsistencyLevel": { + "type": "string", + "metadata": { + "description": "Default Cosmosdb Account level consistency" + } + }, + "enableMultipleRegions": { + "type": "bool", + "metadata": { + "description": "Enable multiple regions, default value is false" + }, + "defaultValue": false + }, + "enablePartitionMerge": { + "type": "bool", + "metadata": { + "description": "Enable partition merge, default value is false" + }, + "defaultValue": false + } + }, + "variables": { + "apiVersion": "2022-08-15", + "accountName": "[toLower(parameters('baseName'))]", + "newAccountName": "[toLower(concat(parameters('baseName'), '2'))]", + "resourceId": "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('accountName'))]", + "newResourceId": "[resourceId('Microsoft.DocumentDB/databaseAccounts', variables('newAccountName'))]", + "singleRegionConfiguration": [ + { + "locationName": "West Central US", + "provisioningState": "Succeeded", + "failoverPriority": 0, + "isZoneRedundant": false + }], + "multiRegionConfiguration": [ + { + "locationName": "West Central US", + "provisioningState": "Succeeded", + "failoverPriority": 0, + "isZoneRedundant": false + }, + { + "locationName": "Central US", + "provisioningState": "Succeeded", + "failoverPriority": 1, + "isZoneRedundant": false + } + ], + "locationsConfiguration": "[if(parameters('enableMultipleRegions'), variables('multiRegionConfiguration'), variables('singleRegionConfiguration'))]" + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "[variables('apiVersion')]", + "name": "[variables('accountName')]", + "location": "[resourceGroup().location]", + "kind": "GlobalDocumentDB", + "properties": { + "publicNetworkAccess": "Enabled", + "enableAutomaticFailover": false, + "enableMultipleWriteLocations": "[parameters('enableMultipleWriteLocations')]", + "enablePartitionMerge": "[parameters('enablePartitionMerge')]", + "isVirtualNetworkFilterEnabled": false, + "virtualNetworkRules": [], + "disableKeyBasedMetadataWriteAccess": false, + "enableFreeTier": false, + "enableAnalyticalStorage": false, + "databaseAccountOfferType": "Standard", + "consistencyPolicy": { + "defaultConsistencyLevel": "[parameters('defaultConsistencyLevel')]", + "maxIntervalInSeconds": 5, + "maxStalenessPrefix": 100 + }, + "locations": "[variables('locationsConfiguration')]", + "capabilities": [], + "ipRules": [] + } + }, + { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "[variables('apiVersion')]", + "name": "[variables('newAccountName')]", + "location": "[resourceGroup().location]", + "kind": "GlobalDocumentDB", + "properties": { + "publicNetworkAccess": "Enabled", + "enableAutomaticFailover": false, + "enableMultipleWriteLocations": "[parameters('enableMultipleWriteLocations')]", + "enablePartitionMerge": "[parameters('enablePartitionMerge')]", + "isVirtualNetworkFilterEnabled": false, + "virtualNetworkRules": [], + "disableKeyBasedMetadataWriteAccess": false, + "enableFreeTier": false, + "enableAnalyticalStorage": false, + "databaseAccountOfferType": "Standard", + "consistencyPolicy": { + "defaultConsistencyLevel": "[parameters('defaultConsistencyLevel')]", + "maxIntervalInSeconds": 5, + "maxStalenessPrefix": 100 + }, + "locations": "[variables('locationsConfiguration')]", + "capabilities": [], + "ipRules": [] + } + } + ], + "outputs": { + "COSMOS_SPRING_ACCOUNT_HOST": { + "type": "string", + "value": "[reference(variables('resourceId'), variables('apiVersion')).documentEndpoint]" + }, + "COSMOS_SPRING_ACCOUNT_KEY": { + "type": "string", + "value": "[listKeys(variables('resourceId'), variables('apiVersion')).primaryMasterKey]" + }, + "COSMOS_SPRING_SECONDARY_ACCOUNT_KEY": { + "type": "string", + "value": "[listKeys(variables('resourceId'), variables('apiVersion')).secondaryMasterKey]" + }, + "COSMOS_SPRING_NEW_ACCOUNT_HOST": { + "type": "string", + "value": "[reference(variables('newResourceId'), variables('apiVersion')).documentEndpoint]" + }, + "COSMOS_SPRING_NEW_ACCOUNT_KEY": { + "type": "string", + "value": "[listKeys(variables('newResourceId'), variables('apiVersion')).primaryMasterKey]" + }, + "COSMOS_SPRING_NEW_SECONDARY_ACCOUNT_KEY": { + "type": "string", + "value": "[listKeys(variables('newResourceId'), variables('apiVersion')).secondaryMasterKey]" + } + } +} diff --git a/sdk/spring/tests.yml b/sdk/spring/tests.yml index ef87feb9b854..41ff49747c6e 100644 --- a/sdk/spring/tests.yml +++ b/sdk/spring/tests.yml @@ -18,6 +18,7 @@ stages: - spring/spring-cloud-azure-integration-tests/test-resources/jdbc/mysql - spring/spring-cloud-azure-integration-tests/test-resources/appconfiguration - spring/spring-cloud-azure-integration-tests/test-resources/cosmos + - spring/spring-cloud-azure-integration-tests/test-resources/cosmos-spring - spring/spring-cloud-azure-integration-tests/test-resources/servicebus - spring/spring-cloud-azure-integration-tests/test-resources/eventhubs - spring/spring-cloud-azure-integration-tests/test-resources/storage From e7aafa94bbbc07cd3326d1c24e3059e439be0348 Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Wed, 10 May 2023 16:08:19 +0800 Subject: [PATCH 2/3] upgrade spring version for Cosmos --- .../azure-spring-data-cosmos-test/pom.xml | 2 +- sdk/spring/azure-spring-data-cosmos/pom.xml | 46 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sdk/spring/azure-spring-data-cosmos-test/pom.xml b/sdk/spring/azure-spring-data-cosmos-test/pom.xml index fd689499a84a..af24c16f3989 100644 --- a/sdk/spring/azure-spring-data-cosmos-test/pom.xml +++ b/sdk/spring/azure-spring-data-cosmos-test/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.9 + 3.0.5 diff --git a/sdk/spring/azure-spring-data-cosmos/pom.xml b/sdk/spring/azure-spring-data-cosmos/pom.xml index 54b43242c22e..561d4c872d2c 100644 --- a/sdk/spring/azure-spring-data-cosmos/pom.xml +++ b/sdk/spring/azure-spring-data-cosmos/pom.xml @@ -41,7 +41,7 @@ org.springframework spring-core - 5.3.25 + 6.0.7 commons-logging @@ -52,27 +52,27 @@ org.springframework spring-web - 5.3.25 + 6.0.7 org.springframework spring-beans - 5.3.25 + 6.0.7 org.springframework spring-context - 5.3.25 + 6.0.7 org.springframework spring-tx - 5.3.25 + 6.0.7 org.springframework.data spring-data-commons - 2.7.8 + 3.0.4 org.slf4j @@ -83,7 +83,7 @@ org.springframework spring-expression - 5.3.25 + 6.0.7 com.azure @@ -93,12 +93,12 @@ com.fasterxml.jackson.module jackson-module-parameter-names - 2.13.5 + 2.14.2 com.fasterxml.jackson.datatype jackson-datatype-jdk8 - 2.13.5 + 2.14.2 org.javatuples @@ -132,7 +132,7 @@ org.springframework.boot spring-boot-starter-test - 2.7.9 + 3.0.5 test @@ -144,13 +144,13 @@ io.projectreactor reactor-test - 3.4.27 + 3.5.4 test org.slf4j slf4j-simple - 1.7.36 + 2.0.7 test + 2.0.7 @@ -183,19 +183,19 @@ - org.springframework:spring-beans:[5.3.25] - org.springframework:spring-web:[5.3.25] - org.springframework:spring-tx:[5.3.25] - org.springframework:spring-expression:[5.3.25] - org.springframework:spring-core:[5.3.25] - org.springframework:spring-context:[5.3.25] - org.springframework.data:spring-data-commons:[2.7.8] + org.springframework:spring-beans:[6.0.7] + org.springframework:spring-web:[6.0.7] + org.springframework:spring-tx:[6.0.7] + org.springframework:spring-expression:[6.0.7] + org.springframework:spring-core:[6.0.7] + org.springframework:spring-context:[6.0.7] + org.springframework.data:spring-data-commons:[3.0.4] org.javatuples:javatuples:[1.2] - com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.13.5] - com.fasterxml.jackson.module:jackson-module-parameter-names:[2.13.5] + com.fasterxml.jackson.datatype:jackson-datatype-jdk8:[2.14.2] + com.fasterxml.jackson.module:jackson-module-parameter-names:[2.14.2] javax.annotation:javax.annotation-api:[1.3.2] org.apache.commons:commons-lang3:[3.12.0] - org.slf4j:slf4j-api:[1.7.36] + org.slf4j:slf4j-api:[2.0.7] From 447029bb98eb15297b4e35bc03177e6456293b4b Mon Sep 17 00:00:00 2001 From: Xiaolu Dai Date: Wed, 10 May 2023 16:20:35 +0800 Subject: [PATCH 3/3] not run ci for spring-data-cosmos temporarily --- sdk/spring/ci.yml | 22 +++++++++++----------- sdk/spring/pom.xml | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml index d71a77ac170b..42c7ad19db40 100644 --- a/sdk/spring/ci.yml +++ b/sdk/spring/ci.yml @@ -27,10 +27,10 @@ pr: # boolean for each library. Note: AdditionalModules are never to have # parameter entries as those are not releasing libraries. parameters: -- name: release_azurespringdatacosmos - displayName: 'azure-spring-data-cosmos' - type: boolean - default: true +#- name: release_azurespringdatacosmos +# displayName: 'azure-spring-data-cosmos' +# type: boolean +# default: true - name: release_springcloudazurecore displayName: 'spring-cloud-azure-core' type: boolean @@ -227,10 +227,10 @@ extends: ServiceDirectory: spring EnableBatchRelease: true Artifacts: - - name: azure-spring-data-cosmos - groupId: com.azure - safeName: azurespringdatacosmos - releaseInBatch: ${{ parameters.release_azurespringdatacosmos }} +# - name: azure-spring-data-cosmos +# groupId: com.azure +# safeName: azurespringdatacosmos +# releaseInBatch: ${{ parameters.release_azurespringdatacosmos }} - name: spring-cloud-azure-core groupId: com.azure.spring safeName: springcloudazurecore @@ -551,8 +551,8 @@ extends: groupId: com.azure.spring safeName: springcloudazurestarterappconfigurationconfig releaseInBatch: ${{ parameters.release_springcloudazurestarterappconfigurationconfig }} - AdditionalModules: - - name: azure-spring-data-cosmos-test - groupId: com.azure +# AdditionalModules: +# - name: azure-spring-data-cosmos-test +# groupId: com.azure MatrixFilters: - JavaTestVersion=^(?!1.8|1.11).* diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml index 388ec7c4c22e..b06e4723fc62 100644 --- a/sdk/spring/pom.xml +++ b/sdk/spring/pom.xml @@ -66,8 +66,8 @@ spring-cloud-azure-feature-management spring-cloud-azure-feature-management-web spring-cloud-azure-starter-appconfiguration-config - azure-spring-data-cosmos - azure-spring-data-cosmos-test + + @@ -126,8 +126,8 @@ spring-cloud-azure-feature-management spring-cloud-azure-feature-management-web spring-cloud-azure-starter-appconfiguration-config - azure-spring-data-cosmos - azure-spring-data-cosmos-test + +