Enable SpEL expressions for @Document collection name (#390)

* Evaluate SpEL expressions for @Document collection name

* Polish SpEL expression resolution for @Document collection

* DocumentDbFactory renamed to CosmosDbFactory

* Fix broken JUnit tests due to wrong ObjectMapper
This commit is contained in:
Domenico Sibilio 2019-09-25 21:35:40 +02:00 коммит произвёл Kushagra Thapar
Родитель b500a9ac7f
Коммит a1f7dd3116
11 изменённых файлов: 298 добавлений и 2 удалений

Просмотреть файл

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

Просмотреть файл

@ -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();

Просмотреть файл

@ -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<T, ID> 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;

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -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";
}

Просмотреть файл

@ -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,12 +28,22 @@ 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() {
final AbstractApplicationContext context = new AnnotationConfigApplicationContext(

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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<SpELPropertyStudent, String> documentDbEntityInfo;
@After
public void cleanUp() {
if (dbTemplate != null && documentDbEntityInfo != null) {
dbTemplate.deleteCollection(documentDbEntityInfo.getCollectionName());
}
}
@Test
public void testDynamicCollectionNameWithPropertySourceExpression() {
final DocumentDbEntityInformation<SpELPropertyStudent, Object> propertyStudentInfo =
new DocumentDbEntityInformation<>(SpELPropertyStudent.class);
assertEquals(TestConstants.DYNAMIC_PROPERTY_COLLECTION_NAME, propertyStudentInfo.getCollectionName());
}
@Test
public void testDynamicCollectionNameWithBeanExpression() {
final DocumentDbEntityInformation<SpELBeanStudent, Object> 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);
}
}

Просмотреть файл

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