diff --git a/src/main/java/com/microsoft/azure/spring/data/cosmosdb/common/ExpressionResolver.java b/src/main/java/com/microsoft/azure/spring/data/cosmosdb/common/ExpressionResolver.java new file mode 100644 index 0000000..bb08ac5 --- /dev/null +++ b/src/main/java/com/microsoft/azure/spring/data/cosmosdb/common/ExpressionResolver.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ + +package com.microsoft.azure.spring.data.cosmosdb.common; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.EmbeddedValueResolver; + +/** + * + * @author Domenico Sibilio + * + */ +public class ExpressionResolver { + + private static EmbeddedValueResolver embeddedValueResolver; + + 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/src/main/java/com/microsoft/azure/spring/data/cosmosdb/config/DocumentDbConfigurationSupport.java b/src/main/java/com/microsoft/azure/spring/data/cosmosdb/config/DocumentDbConfigurationSupport.java index 2425814..53b98d8 100644 --- a/src/main/java/com/microsoft/azure/spring/data/cosmosdb/config/DocumentDbConfigurationSupport.java +++ b/src/main/java/com/microsoft/azure/spring/data/cosmosdb/config/DocumentDbConfigurationSupport.java @@ -6,7 +6,10 @@ package com.microsoft.azure.spring.data.cosmosdb.config; +import com.microsoft.azure.spring.data.cosmosdb.common.ExpressionResolver; import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentDbMappingContext; + +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; @@ -21,10 +24,19 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; public abstract class DocumentDbConfigurationSupport { + @Bean + public ExpressionResolver expressionResolver(BeanFactory beanFactory) { + return new ExpressionResolver(beanFactory); + } + @Bean public DocumentDbMappingContext documentDbMappingContext() throws ClassNotFoundException { final DocumentDbMappingContext mappingContext = new DocumentDbMappingContext(); diff --git a/src/main/java/com/microsoft/azure/spring/data/cosmosdb/repository/support/DocumentDbEntityInformation.java b/src/main/java/com/microsoft/azure/spring/data/cosmosdb/repository/support/DocumentDbEntityInformation.java index 80361e9..a2463e5 100644 --- a/src/main/java/com/microsoft/azure/spring/data/cosmosdb/repository/support/DocumentDbEntityInformation.java +++ b/src/main/java/com/microsoft/azure/spring/data/cosmosdb/repository/support/DocumentDbEntityInformation.java @@ -15,11 +15,14 @@ import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy; import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey; import org.apache.commons.lang3.reflect.FieldUtils; + import org.springframework.data.annotation.Id; import org.springframework.data.repository.core.support.AbstractEntityInformation; import org.springframework.lang.NonNull; import org.springframework.util.ReflectionUtils; +import static com.microsoft.azure.spring.data.cosmosdb.common.ExpressionResolver.resolveExpression; + import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; @@ -135,7 +138,7 @@ public class DocumentDbEntityInformation extends AbstractEntityInformatio final Document annotation = domainClass.getAnnotation(Document.class); if (annotation != null && annotation.collection() != null && !annotation.collection().isEmpty()) { - customCollectionName = annotation.collection(); + customCollectionName = resolveExpression(annotation.collection()); } return customCollectionName; diff --git a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/common/ExpressionResolverUnitTest.java b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/common/ExpressionResolverUnitTest.java new file mode 100644 index 0000000..690b823 --- /dev/null +++ b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/common/ExpressionResolverUnitTest.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.data.cosmosdb.common; + +import org.junit.Test; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * + * @author Domenico Sibilio + * + */ +public class ExpressionResolverUnitTest { + private static final String LITERAL_EXPRESSION = "literal expression"; + private static final String SPEL_EXPRESSION = "#{@environment.getProperty('dynamic.collection.name')}"; + + @Test + public void testLiteralExpressionsShouldNotBeAltered() { + assertEquals(LITERAL_EXPRESSION, ExpressionResolver.resolveExpression(LITERAL_EXPRESSION)); + } + + @Test + public void testExpressionsShouldBeResolved() { + final AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(TestConfiguration.class); + + assertNotNull(applicationContext.getBean(ExpressionResolver.class)); + assertEquals(TestConstants.DYNAMIC_PROPERTY_COLLECTION_NAME, + ExpressionResolver.resolveExpression(SPEL_EXPRESSION)); + } + + @Configuration + @PropertySource("application.properties") + static class TestConfiguration { + @Bean + public ExpressionResolver expressionResolver(ConfigurableBeanFactory beanFactory) { + return new ExpressionResolver((ConfigurableBeanFactory) beanFactory); + } + } + +} diff --git a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/common/TestConstants.java b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/common/TestConstants.java index 7e7c5ce..6dff4e0 100644 --- a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/common/TestConstants.java +++ b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/common/TestConstants.java @@ -151,5 +151,9 @@ public class TestConstants { 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"; + } diff --git a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/config/AbstractDocumentDbConfigurationIT.java b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/config/AbstractDocumentDbConfigurationIT.java index f35a21d..cbf6e8e 100644 --- a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/config/AbstractDocumentDbConfigurationIT.java +++ b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/config/AbstractDocumentDbConfigurationIT.java @@ -12,12 +12,14 @@ import com.microsoft.azure.documentdb.DocumentClient; import com.microsoft.azure.documentdb.RequestOptions; import com.microsoft.azure.spring.data.cosmosdb.Constants; import com.microsoft.azure.spring.data.cosmosdb.DocumentDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.ExpressionResolver; import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.Mock; + import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -26,11 +28,21 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.context.support.AbstractApplicationContext; +import static org.junit.Assert.assertNotNull; + public class AbstractDocumentDbConfigurationIT { private static final String OBJECTMAPPER_BEAN_NAME = Constants.OBJECTMAPPER_BEAN_NAME; @Rule public ExpectedException exception = ExpectedException.none(); + + @Test + public void containsExpressionResolver() { + final AbstractApplicationContext context = new AnnotationConfigApplicationContext( + TestDocumentDbConfiguration.class); + + assertNotNull(context.getBean(ExpressionResolver.class)); + } @Test public void containsDocumentDbFactory() { diff --git a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/domain/SpELBeanStudent.java b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/domain/SpELBeanStudent.java new file mode 100644 index 0000000..4d450da --- /dev/null +++ b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/domain/SpELBeanStudent.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ + +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Document(collection = "#{@dynamicCollectionContainer.getCollectionName()}") +public class SpELBeanStudent { + private String id; + private String firstName; + private String lastName; +} diff --git a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/domain/SpELPropertyStudent.java b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/domain/SpELPropertyStudent.java new file mode 100644 index 0000000..ce2630f --- /dev/null +++ b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/domain/SpELPropertyStudent.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ + +package com.microsoft.azure.spring.data.cosmosdb.domain; + +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Document(collection = "${dynamic.collection.name}") +public class SpELPropertyStudent { + private String id; + private String firstName; + private String lastName; +} diff --git a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/repository/TestRepositoryConfig.java b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/repository/TestRepositoryConfig.java index 329550a..282d3d2 100644 --- a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/repository/TestRepositoryConfig.java +++ b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/repository/TestRepositoryConfig.java @@ -54,4 +54,21 @@ public class TestRepositoryConfig extends AbstractDocumentDbConfiguration { return DocumentDBConfig.builder(connectionString, dbName).requestOptions(options).build(); } + + @Bean + public DynamicCollectionContainer dynamicCollectionContainer() { + return new DynamicCollectionContainer("spel-bean-collection"); + } + + public class DynamicCollectionContainer { + private String collectionName; + + public DynamicCollectionContainer(String collectionName) { + this.collectionName = collectionName; + } + + public String getCollectionName() { + return this.collectionName; + } + } } diff --git a/src/test/java/com/microsoft/azure/spring/data/cosmosdb/repository/integration/SpELDocumentDBAnnotationIT.java b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/repository/integration/SpELDocumentDBAnnotationIT.java new file mode 100644 index 0000000..ce91482 --- /dev/null +++ b/src/test/java/com/microsoft/azure/spring/data/cosmosdb/repository/integration/SpELDocumentDBAnnotationIT.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for + * license information. + */ +package com.microsoft.azure.spring.data.cosmosdb.repository.integration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.spring.data.cosmosdb.CosmosDbFactory; +import com.microsoft.azure.spring.data.cosmosdb.common.TestConstants; +import com.microsoft.azure.spring.data.cosmosdb.config.DocumentDBConfig; +import com.microsoft.azure.spring.data.cosmosdb.core.DocumentDbTemplate; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.MappingDocumentDbConverter; +import com.microsoft.azure.spring.data.cosmosdb.core.convert.ObjectMapperFactory; +import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentDbMappingContext; +import com.microsoft.azure.spring.data.cosmosdb.domain.SpELBeanStudent; +import com.microsoft.azure.spring.data.cosmosdb.domain.SpELPropertyStudent; +import com.microsoft.azure.spring.data.cosmosdb.repository.TestRepositoryConfig; +import com.microsoft.azure.spring.data.cosmosdb.repository.support.DocumentDbEntityInformation; +import org.junit.After; +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; + +/** + * + * @author Domenico Sibilio + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestRepositoryConfig.class) +public class SpELDocumentDBAnnotationIT { + private static final SpELPropertyStudent TEST_PROPERTY_STUDENT = + new SpELPropertyStudent(TestConstants.ID_1, TestConstants.FIRST_NAME, + TestConstants.LAST_NAME); + + @Value("${cosmosdb.uri}") + private String dbUri; + + @Value("${cosmosdb.key}") + private String dbKey; + + @Autowired + private ApplicationContext applicationContext; + + private DocumentDbTemplate dbTemplate; + private DocumentDbEntityInformation documentDbEntityInfo; + + @After + public void cleanUp() { + if (dbTemplate != null && documentDbEntityInfo != null) { + dbTemplate.deleteCollection(documentDbEntityInfo.getCollectionName()); + } + } + + @Test + public void testDynamicCollectionNameWithPropertySourceExpression() { + final DocumentDbEntityInformation propertyStudentInfo = + new DocumentDbEntityInformation<>(SpELPropertyStudent.class); + + assertEquals(TestConstants.DYNAMIC_PROPERTY_COLLECTION_NAME, propertyStudentInfo.getCollectionName()); + } + + @Test + public void testDynamicCollectionNameWithBeanExpression() { + final DocumentDbEntityInformation beanStudentInfo = + new DocumentDbEntityInformation<>(SpELBeanStudent.class); + + assertEquals(TestConstants.DYNAMIC_BEAN_COLLECTION_NAME, beanStudentInfo.getCollectionName()); + } + + @Test + public void testDatabaseOperationsOnDynamicallyNamedCollection() throws ClassNotFoundException { + final DocumentDBConfig dbConfig = DocumentDBConfig.builder(dbUri, dbKey, TestConstants.DB_NAME).build(); + final CosmosDbFactory dbFactory = new CosmosDbFactory(dbConfig); + + documentDbEntityInfo = new DocumentDbEntityInformation<>(SpELPropertyStudent.class); + final DocumentDbMappingContext dbContext = new DocumentDbMappingContext(); + dbContext.setInitialEntitySet(new EntityScanner(this.applicationContext).scan(Persistent.class)); + + final ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper(); + final MappingDocumentDbConverter mappingConverter = new MappingDocumentDbConverter(dbContext, objectMapper); + dbTemplate = new DocumentDbTemplate(dbFactory, mappingConverter, TestConstants.DB_NAME); + + dbTemplate.createCollectionIfNotExists(documentDbEntityInfo); + + final SpELPropertyStudent insertedRecord = + dbTemplate.insert(documentDbEntityInfo.getCollectionName(), TEST_PROPERTY_STUDENT, null); + assertNotNull(insertedRecord); + + final SpELPropertyStudent readRecord = + dbTemplate.findById(TestConstants.DYNAMIC_PROPERTY_COLLECTION_NAME, + insertedRecord.getId(), SpELPropertyStudent.class); + assertNotNull(readRecord); + } + +} + diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 953ad63..1a70c6b 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -5,6 +5,7 @@ cosmosdb.secondaryKey=${COSMOSDB_SECONDARY_KEY} #You can also use connection string instead of uri and key to connect to cosmos DB #cosmosdb.connection-string=${DOCUMENTDB_CONNECTION_STRING} +dynamic.collection.name=spel-property-collection # Performance test configurations perf.recursive.times=10 perf.batch.size=3