diff --git a/pom.xml b/pom.xml index 477fa5f6..fa27ad62 100755 --- a/pom.xml +++ b/pom.xml @@ -101,6 +101,11 @@ DynamoDBLocal [1.11,2.0) + + org.apache.logging.log4j + log4j-to-slf4j + 2.8.2 + uk.org.lidalia @@ -175,7 +180,12 @@ com.amazonaws DynamoDBLocal test - + + + org.apache.logging.log4j + log4j-to-slf4j + test + junit junit @@ -248,6 +258,39 @@ maven-jar-plugin 3.0.2 + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + net.revelc.code.formatter + + + formatter-maven-plugin + + + [2.7.2,) + + + validate + + + + + true + + + + + + + @@ -401,6 +444,7 @@ Max + src/spotbugs-exclude.xml @@ -434,26 +478,6 @@ org.apache.maven.plugins maven-dependency-plugin - - - unpack-dynamodb-local - - unpack - - pre-integration-test - - - - com.jcabi - DynamoDBLocal - 2015-07-16 - zip - ${project.build.directory}/dynamodb-dist - false - - - - copy @@ -476,60 +500,6 @@ - - org.codehaus.mojo - build-helper-maven-plugin - - - reserver-dynamodb-port - - reserve-network-port - - pre-integration-test - - - dynamodblocal.port - - - - - - - com.jcabi - jcabi-dynamodb-maven-plugin - 0.9.1 - - - src/test/resources/user_table.json
- src/test/resources/playlist_table.json
- src/test/resources/feeduser_table.json
- src/test/resources/customerhistory_table.json
- src/test/resources/installation_table.json
- src/test/resources/auditable_user_table.json
-
- ${dynamodblocal.port} - ${project.build.directory}/dynamodb-dist - - -inMemory - -
- - - pre-integration-test - - start - create-tables - - - - post-integration-test - - stop - - post-integration-test - - -
org.apache.maven.plugins maven-surefire-plugin diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplate.java b/src/main/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplate.java index c7ad1d39..f1f47bdf 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplate.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplate.java @@ -39,6 +39,7 @@ import org.socialsignin.spring.data.dynamodb.mapping.event.BeforeSaveEvent; import org.socialsignin.spring.data.dynamodb.mapping.event.DynamoDBMappingEvent; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; @@ -57,42 +58,6 @@ public class DynamoDBTemplate implements DynamoDBOperations, ApplicationContextA private final DynamoDBMapperConfig dynamoDBMapperConfig; private ApplicationEventPublisher eventPublisher; - /** - * Convenient constructor to use the default - * {@link DynamoDBMapper#DynamoDBMapper(AmazonDynamoDB)} - * - * @param amazonDynamoDB - * The AWS SDK instance to talk to DynamoDB - * @param dynamoDBMapperConfig - * The config to use - */ - public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB, DynamoDBMapperConfig dynamoDBMapperConfig) { - this(amazonDynamoDB, dynamoDBMapperConfig, null); - } - - /** - * Convenient constructor to use the {@link DynamoDBMapperConfig#DEFAULT} - * - * @param amazonDynamoDB - * The AWS SDK instance to talk to DynamoDB - * @param dynamoDBMapper - * The Mapper to use - */ - public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB, DynamoDBMapper dynamoDBMapper) { - this(amazonDynamoDB, null, dynamoDBMapper); - } - - /** - * Convenient construcotr to thse the {@link DynamoDBMapperConfig#DEFAULT} and - * default {@link DynamoDBMapper#DynamoDBMapper(AmazonDynamoDB)} - * - * @param amazonDynamoDB - * The AWS SDK instance to talk to DynamoDB - */ - public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB) { - this(amazonDynamoDB, null, null); - } - /** * Initializes a new {@code DynamoDBTemplate}. The following combinations are * valid: @@ -106,49 +71,18 @@ public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB) { * can be {@code null} - * {@link DynamoDBMapper#DynamoDBMapper(AmazonDynamoDB, DynamoDBMapperConfig)} * is used if {@code null} is passed in + * @param dynamoDBMapperConfig */ - public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB, DynamoDBMapperConfig dynamoDBMapperConfig, - DynamoDBMapper dynamoDBMapper) { + @Autowired + public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB, DynamoDBMapper dynamoDBMapper, + DynamoDBMapperConfig dynamoDBMapperConfig) { Assert.notNull(amazonDynamoDB, "amazonDynamoDB must not be null!"); - this.amazonDynamoDB = amazonDynamoDB; + Assert.notNull(dynamoDBMapper, "dynamoDBMapper must not be null!"); + Assert.notNull(dynamoDBMapperConfig, "dynamoDBMapperConfig must not be null!"); - if (dynamoDBMapperConfig == null) { - this.dynamoDBMapperConfig = DynamoDBMapperConfig.DEFAULT; - } else { - - // #146, #81 #157 - // Trying to fix half-initialized DynamoDBMapperConfigs here. - // The old documentation advised to start with an empty builder. Therefore we - // try here to set required fields to their defaults - - // As the documentation at - // https://github.com/derjust/spring-data-dynamodb/wiki/Alter-table-name-during-runtime - // (same as https://git.io/DynamoDBMapperConfig) - // now does: Start with #DEFAULT and add what's required - DynamoDBMapperConfig.Builder emptyBuilder = DynamoDBMapperConfig.builder(); // empty (!) builder - - if (dynamoDBMapperConfig.getConversionSchema() == null) { - LOGGER.warn( - "No ConversionSchema set in the provided dynamoDBMapperConfig! Merging with DynamoDBMapperConfig.DEFAULT - Please see https://git.io/DynamoDBMapperConfig"); - // DynamoDBMapperConfig#DEFAULT comes with a ConversionSchema - emptyBuilder.withConversionSchema(DynamoDBMapperConfig.DEFAULT.getConversionSchema()); - } - - if (dynamoDBMapperConfig.getTypeConverterFactory() == null) { - LOGGER.warn( - "No TypeConverterFactory set in the provided dynamoDBMapperConfig! Merging with DynamoDBMapperConfig.DEFAULT - Please see https://git.io/DynamoDBMapperConfig"); - // DynamoDBMapperConfig#DEFAULT comes with a TypeConverterFactory - emptyBuilder.withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()); - } - - // Deprecated but the only way how DynamoDBMapperConfig#merge is exposed - this.dynamoDBMapperConfig = new DynamoDBMapperConfig(dynamoDBMapperConfig, emptyBuilder.build()); - } - - if (dynamoDBMapper == null) { - this.dynamoDBMapper = new DynamoDBMapper(amazonDynamoDB, dynamoDBMapperConfig); - } else { - this.dynamoDBMapper = dynamoDBMapper; - } + this.amazonDynamoDB = amazonDynamoDB; + this.dynamoDBMapper = dynamoDBMapper; + this.dynamoDBMapperConfig = dynamoDBMapperConfig; } @Override diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/cdi/DynamoDBRepositoryBean.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/cdi/DynamoDBRepositoryBean.java index 2adc57fc..5104a68f 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/cdi/DynamoDBRepositoryBean.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/cdi/DynamoDBRepositoryBean.java @@ -16,6 +16,7 @@ package org.socialsignin.spring.data.dynamodb.repository.cdi; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; import org.socialsignin.spring.data.dynamodb.core.DynamoDBOperations; import org.socialsignin.spring.data.dynamodb.core.DynamoDBTemplate; @@ -100,8 +101,13 @@ protected T create(CreationalContext creationalContext, Class repositoryTy ? null : getDependencyInstance(dynamoDBOperationsBean, DynamoDBOperations.class); + if (dynamoDBMapperConfig == null) { + dynamoDBMapperConfig = DynamoDBMapperConfig.DEFAULT; + } + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(amazonDynamoDB, dynamoDBMapperConfig); + if (dynamoDBOperations == null) { - dynamoDBOperations = new DynamoDBTemplate(amazonDynamoDB, dynamoDBMapperConfig); + dynamoDBOperations = new DynamoDBTemplate(amazonDynamoDB, dynamoDBMapper, dynamoDBMapperConfig); } DynamoDBRepositoryFactory factory = new DynamoDBRepositoryFactory(dynamoDBOperations); diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBMapperConfigFactory.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBMapperConfigFactory.java new file mode 100644 index 00000000..36340e56 --- /dev/null +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBMapperConfigFactory.java @@ -0,0 +1,83 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.repository.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.lang.Nullable; + +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; + +public class DynamoDBMapperConfigFactory implements FactoryBean, BeanPostProcessor { + private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBMapperConfigFactory.class); + @Override + public DynamoDBMapperConfig getObject() throws Exception { + return DynamoDBMapperConfig.DEFAULT; + } + + @Override + public Class getObjectType() { + return DynamoDBMapperConfig.class; + } + + @Nullable + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof DynamoDBMapperConfig) { + DynamoDBMapperConfig dynamoDBMapperConfig = (DynamoDBMapperConfig) bean; + if (dynamoDBMapperConfig == DynamoDBMapperConfig.DEFAULT) { + return bean; + } + // #146, #81 #157 + // Trying to fix half-initialized DynamoDBMapperConfigs here. + // The old documentation advised to start with an empty builder. Therefore we + // try here to set required fields to their defaults - + // As the documentation at + // https://github.com/derjust/spring-data-dynamodb/wiki/Alter-table-name-during-runtime + // (same as https://git.io/DynamoDBMapperConfig) + // now does: Start with #DEFAULT and add what's required + DynamoDBMapperConfig.Builder emptyBuilder = DynamoDBMapperConfig.builder(); // empty (!) builder + + if (dynamoDBMapperConfig.getConversionSchema() == null) { + LOGGER.warn( + "No ConversionSchema set in the provided dynamoDBMapperConfig! Merging with DynamoDBMapperConfig.DEFAULT - Please see https://git.io/DynamoDBMapperConfig"); + // DynamoDBMapperConfig#DEFAULT comes with a ConversionSchema + emptyBuilder.withConversionSchema(DynamoDBMapperConfig.DEFAULT.getConversionSchema()); + } + + if (dynamoDBMapperConfig.getTypeConverterFactory() == null) { + LOGGER.warn( + "No TypeConverterFactory set in the provided dynamoDBMapperConfig! Merging with DynamoDBMapperConfig.DEFAULT - Please see https://git.io/DynamoDBMapperConfig"); + // DynamoDBMapperConfig#DEFAULT comes with a TypeConverterFactory + emptyBuilder.withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()); + } + + return createDynamoDBMapperConfig(dynamoDBMapperConfig, emptyBuilder); + + } else { + return bean; + } + } + + @SuppressWarnings("deprecation") + private DynamoDBMapperConfig createDynamoDBMapperConfig(DynamoDBMapperConfig dynamoDBMapperConfig, + DynamoDBMapperConfig.Builder emptyBuilder) { + // Deprecated but the only way how DynamoDBMapperConfig#merge is exposed + return new DynamoDBMapperConfig(dynamoDBMapperConfig, emptyBuilder.build()); + } +} diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBMapperFactory.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBMapperFactory.java new file mode 100644 index 00000000..20486acc --- /dev/null +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBMapperFactory.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.repository.config; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Autowired; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; + +public class DynamoDBMapperFactory implements FactoryBean { + + private final AmazonDynamoDB amazonDynamoDB; + private final DynamoDBMapperConfig dynamoDBMapperConfig; + + @Autowired + public DynamoDBMapperFactory(AmazonDynamoDB amazonDynamoDB, DynamoDBMapperConfig dynamoDBMapperConfig) { + this.amazonDynamoDB = amazonDynamoDB; + this.dynamoDBMapperConfig = dynamoDBMapperConfig; + } + + @Override + public DynamoDBMapper getObject() throws Exception { + return new DynamoDBMapper(amazonDynamoDB, dynamoDBMapperConfig); + } + + @Override + public Class getObjectType() { + return DynamoDBMapper.class; + } + +} diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBRepositoryConfigExtension.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBRepositoryConfigExtension.java index c691de90..af5fae4b 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBRepositoryConfigExtension.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBRepositoryConfigExtension.java @@ -15,13 +15,23 @@ */ package org.socialsignin.spring.data.dynamodb.repository.config; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.socialsignin.spring.data.dynamodb.core.DynamoDBTemplate; import org.socialsignin.spring.data.dynamodb.mapping.DynamoDBMappingContext; import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBRepositoryFactoryBean; +import org.socialsignin.spring.data.dynamodb.repository.util.DynamoDBMappingContextProcessor; +import org.socialsignin.spring.data.dynamodb.repository.util.Entity2DynamoDBTableSynchronizer; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.data.config.ParsingUtils; 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.config.XmlRepositoryConfigurationSource; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -33,6 +43,8 @@ */ public class DynamoDBRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { + private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBRepositoryConfigExtension.class); + private static final String DEFAULT_AMAZON_DYNAMO_DB_BEAN_NAME = "amazonDynamoDB"; private static final String DYNAMO_DB_MAPPER_CONFIG_REF = "dynamodb-mapper-config-ref"; @@ -43,6 +55,9 @@ public class DynamoDBRepositoryConfigExtension extends RepositoryConfigurationEx private static final String MAPPING_CONTEXT_REF = "mapping-context-ref"; + private BeanDefinitionRegistry registry; + private String defaultDynamoDBMappingContext; + @Override public String getRepositoryFactoryBeanClassName() { return DynamoDBRepositoryFactoryBean.class.getName(); @@ -52,9 +67,11 @@ public String getRepositoryFactoryBeanClassName() { public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) { AnnotationAttributes attributes = config.getAttributes(); - postProcess(builder, attributes.getString("amazonDynamoDBRef"), attributes.getString("dynamoDBMapperConfigRef"), - attributes.getString("dynamoDBOperationsRef"), attributes.getString("mappingContextRef")); + String repositoryBeanName = config.generateBeanName(builder.getBeanDefinition()); + postProcess(builder, repositoryBeanName, attributes.getString("amazonDynamoDBRef"), + attributes.getString("dynamoDBMapperConfigRef"), attributes.getString("dynamoDBOperationsRef"), + attributes.getString("mappingContextRef")); } /* @@ -68,18 +85,28 @@ public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfi */ @Override public void postProcess(BeanDefinitionBuilder builder, XmlRepositoryConfigurationSource config) { - Element element = config.getElement(); ParsingUtils.setPropertyReference(builder, element, AMAZON_DYNAMODB_REF, "amazonDynamoDB"); ParsingUtils.setPropertyReference(builder, element, DYNAMO_DB_MAPPER_CONFIG_REF, "dynamoDBMapperConfig"); ParsingUtils.setPropertyReference(builder, element, DYNAMO_DB_OPERATIONS_REF, "dynamoDBOperations"); - ParsingUtils.setPropertyReference(builder, element, MAPPING_CONTEXT_REF, "dynamoDBMappingContext"); + String dynamoDBMappingContextRef = element.getAttribute(MAPPING_CONTEXT_REF); + + if (!StringUtils.hasText(dynamoDBMappingContextRef)) { + // Register DynamoDBMappingContext only once if necessary + if (defaultDynamoDBMappingContext == null) { + defaultDynamoDBMappingContext = registerDynamoDBMappingContext(registry); + } + dynamoDBMappingContextRef = defaultDynamoDBMappingContext; + + } + registerAndSetPostProcessingBeans(builder, registry, dynamoDBMappingContextRef); } - private void postProcess(BeanDefinitionBuilder builder, String amazonDynamoDBRef, String dynamoDBMapperConfigRef, - String dynamoDBOperationsRef, String dynamoDBMappingContextRef) { + private Map dynamoDBTemplateCache = new HashMap<>(); + private void postProcess(BeanDefinitionBuilder builder, String repositoryName, String amazonDynamoDBRef, + String dynamoDBMapperConfigRef, String dynamoDBOperationsRef, String dynamoDBMappingContextRef) { if (StringUtils.hasText(dynamoDBOperationsRef)) { builder.addPropertyReference("dynamoDBOperations", dynamoDBOperationsRef); @@ -89,22 +116,129 @@ private void postProcess(BeanDefinitionBuilder builder, String amazonDynamoDBRef "Cannot specify both dynamoDBMapperConfigBean bean and dynamoDBOperationsBean in repository configuration"); } else { - amazonDynamoDBRef = StringUtils.hasText(amazonDynamoDBRef) - ? amazonDynamoDBRef - : DEFAULT_AMAZON_DYNAMO_DB_BEAN_NAME; + if (StringUtils.isEmpty(dynamoDBOperationsRef)) { + + String dynamoDBRef; + if (StringUtils.hasText(amazonDynamoDBRef)) { + dynamoDBRef = amazonDynamoDBRef; + } else { + dynamoDBRef = DEFAULT_AMAZON_DYNAMO_DB_BEAN_NAME; + } - builder.addPropertyReference("amazonDynamoDB", amazonDynamoDBRef); + dynamoDBOperationsRef = dynamoDBTemplateCache + .computeIfAbsent(getBeanNameWithModulePrefix("DynamoDBTemplate-" + dynamoDBRef), ref -> { + BeanDefinitionBuilder dynamoDBTemplateBuilder = BeanDefinitionBuilder + .genericBeanDefinition(DynamoDBTemplate.class); + dynamoDBTemplateBuilder.addConstructorArgReference(dynamoDBRef); + + registry.registerBeanDefinition(ref, dynamoDBTemplateBuilder.getBeanDefinition()); + return ref; + }); + } + + builder.addPropertyReference("dynamoDBOperations", dynamoDBOperationsRef); if (StringUtils.hasText(dynamoDBMapperConfigRef)) { builder.addPropertyReference("dynamoDBMapperConfig", dynamoDBMapperConfigRef); } } - if (StringUtils.hasText(dynamoDBMappingContextRef)) { - builder.addPropertyReference("dynamoDBMappingContext", dynamoDBMappingContextRef); - } else { - builder.addPropertyValue("dynamoDBMappingContext", new DynamoDBMappingContext()); + if (!StringUtils.hasText(dynamoDBMappingContextRef)) { + // Register DynamoDBMappingContext only once if necessary + if (defaultDynamoDBMappingContext == null) { + defaultDynamoDBMappingContext = registerDynamoDBMappingContext(registry); + } + dynamoDBMappingContextRef = defaultDynamoDBMappingContext; + } + + builder.addPropertyReference("dynamoDBMappingContext", dynamoDBMappingContextRef); + registerAndSetPostProcessingBeans(builder, registry, dynamoDBMappingContextRef); + } + + protected void registerAndSetPostProcessingBeans(BeanDefinitionBuilder builder, BeanDefinitionRegistry registry, + String dynamoDBMappingContextRef) { + String tableSynchronizerName = registerEntity2DynamoDBTableSynchronizer(registry, dynamoDBMappingContextRef); + builder.addPropertyReference("entity2DynamoDBTableSynchronizer", tableSynchronizerName); + + String dynamoDBMappingContextProcessorName = registerDynamoDBMappingContextProcessor(registry, + dynamoDBMappingContextRef); + builder.addPropertyReference("dynamoDBMappingContextProcessor", dynamoDBMappingContextProcessorName); + } + + private final Map entity2DynamoDBTableSynchronizerCache = new ConcurrentHashMap<>(); + private String registerEntity2DynamoDBTableSynchronizer(BeanDefinitionRegistry registry, + String dynamoDBMappingContextRef) { + assert registry != null; + + return entity2DynamoDBTableSynchronizerCache.computeIfAbsent(dynamoDBMappingContextRef, ref -> { + BeanDefinitionBuilder entity2DynamoDBTableSynchronizerBuilder = BeanDefinitionBuilder + .genericBeanDefinition(Entity2DynamoDBTableSynchronizer.class); + String tableSynchronizerName = getBeanNameWithModulePrefix( + "Entity2DynamoDBTableSynchronizer-" + dynamoDBMappingContextRef); + registry.registerBeanDefinition(tableSynchronizerName, + entity2DynamoDBTableSynchronizerBuilder.getBeanDefinition()); + + return tableSynchronizerName; + }); + } + + private final Map dynamoDBMappingContextProcessorCache = new ConcurrentHashMap<>(); + private String registerDynamoDBMappingContextProcessor(BeanDefinitionRegistry registry, + String dynamoDBMappingContextRef) { + assert registry != null; + + return dynamoDBMappingContextProcessorCache.computeIfAbsent(dynamoDBMappingContextRef, ref -> { + BeanDefinitionBuilder dynamoDBMappingContextProcessorBuilder = BeanDefinitionBuilder + .genericBeanDefinition(DynamoDBMappingContextProcessor.class); + dynamoDBMappingContextProcessorBuilder.addConstructorArgReference(dynamoDBMappingContextRef); + + String dynamoDBMappingContextProcessorRef = getBeanNameWithModulePrefix( + "DynamoDBMappingContextProcessor-" + dynamoDBMappingContextRef); + registry.registerBeanDefinition(dynamoDBMappingContextProcessorRef, + dynamoDBMappingContextProcessorBuilder.getBeanDefinition()); + + return dynamoDBMappingContextProcessorRef; + + }); + } + + private String registerDynamoDBMappingContext(BeanDefinitionRegistry registry) { + assert registry != null; + + BeanDefinitionBuilder dynamoDBMappingContextBuilder = BeanDefinitionBuilder + .genericBeanDefinition(DynamoDBMappingContext.class); + String dynamoDBMappingContextRef = getBeanNameWithModulePrefix("DynamoDBMappingContext"); + + LOGGER.debug("Adding bean <{}> of type <{}>", dynamoDBMappingContextRef, + dynamoDBMappingContextBuilder.getBeanDefinition()); + + registry.registerBeanDefinition(dynamoDBMappingContextRef, dynamoDBMappingContextBuilder.getBeanDefinition()); + + return dynamoDBMappingContextRef; + } + + @Override + public void registerBeansForRoot(BeanDefinitionRegistry registry, + RepositoryConfigurationSource configurationSource) { + super.registerBeansForRoot(registry, configurationSource); + + // Store for later to be used by #postProcess, too + this.registry = registry; + + BeanDefinitionBuilder dynamoDBMapperConfigBuiilder = BeanDefinitionBuilder + .genericBeanDefinition(DynamoDBMapperConfigFactory.class); + registry.registerBeanDefinition(getBeanNameWithModulePrefix("DynamoDBMapperConfig"), + dynamoDBMapperConfigBuiilder.getBeanDefinition()); + + BeanDefinitionBuilder dynamoDBMapperBuilder = BeanDefinitionBuilder + .genericBeanDefinition(DynamoDBMapperFactory.class); + registry.registerBeanDefinition(getBeanNameWithModulePrefix("DynamoDBMapper"), + dynamoDBMapperBuilder.getBeanDefinition()); + } + + protected String getBeanNameWithModulePrefix(String baseBeanName) { + return String.format("%s-%s", getModulePrefix(), baseBeanName); } @Override diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/EnableDynamoDBRepositories.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/EnableDynamoDBRepositories.java index bb2f9190..a5e68afc 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/EnableDynamoDBRepositories.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/config/EnableDynamoDBRepositories.java @@ -15,11 +15,6 @@ */ package org.socialsignin.spring.data.dynamodb.repository.config; -import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBRepositoryFactoryBean; -import org.springframework.context.annotation.ComponentScan.Filter; -import org.springframework.context.annotation.Import; -import org.springframework.data.repository.query.QueryLookupStrategy.Key; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -27,6 +22,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBRepositoryFactoryBean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Import; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; + /** * Annotation to enable DynamoDB repositories. Will scan the package of the * annotated configuration class for Spring Data repositories by default. diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCriteria.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCriteria.java index 21667d95..5ea4087c 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCriteria.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/query/AbstractDynamoDBQueryCriteria.java @@ -180,8 +180,8 @@ protected void applySortIfSpecified(QueryRequest queryRequest, List perm queryRequest.setScanIndexForward(order.getDirection().equals(Direction.ASC)); sortAlreadySet = true; } else { - throw new UnsupportedOperationException( - "Sorting only possible by " + permittedPropertyNames + " for the criteria specified"); + throw new UnsupportedOperationException("Sorting only possible by " + permittedPropertyNames + + " for the criteria specified and not for " + order.getProperty()); } } } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactory.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactory.java index 8e589597..81b6e0c2 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactory.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactory.java @@ -30,7 +30,6 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.util.Version; -import java.io.Serializable; import java.util.Optional; import java.util.StringTokenizer; @@ -96,13 +95,12 @@ protected static boolean isCompatible(String spec, String impl) { public DynamoDBRepositoryFactory(DynamoDBOperations dynamoDBOperations) { this.dynamoDBOperations = dynamoDBOperations; - } @Override public DynamoDBEntityInformation getEntityInformation(final Class domainClass) { - final DynamoDBEntityMetadataSupport metadata = new DynamoDBEntityMetadataSupport(domainClass); + final DynamoDBEntityMetadataSupport metadata = new DynamoDBEntityMetadataSupport<>(domainClass); return metadata.getEntityInformation(); } @@ -126,8 +124,7 @@ protected Optional getQueryLookupStrategy(Key key, * @return the created {@link DynamoDBCrudRepository} instance */ @SuppressWarnings({"unchecked", "rawtypes"}) - protected DynamoDBCrudRepository getDynamoDBRepository( - RepositoryMetadata metadata) { + protected DynamoDBCrudRepository getDynamoDBRepository(RepositoryMetadata metadata) { return new SimpleDynamoDBPagingAndSortingRepository(getEntityInformation(metadata.getDomainType()), dynamoDBOperations, getEnableScanPermissions(metadata)); } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactoryBean.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactoryBean.java index c9dcc290..66f46624 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactoryBean.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactoryBean.java @@ -15,14 +15,11 @@ */ package org.socialsignin.spring.data.dynamodb.repository.support; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; -import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; import org.socialsignin.spring.data.dynamodb.core.DynamoDBOperations; -import org.socialsignin.spring.data.dynamodb.core.DynamoDBTemplate; import org.socialsignin.spring.data.dynamodb.mapping.DynamoDBMappingContext; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; +import org.socialsignin.spring.data.dynamodb.repository.util.DynamoDBMappingContextProcessor; +import org.socialsignin.spring.data.dynamodb.repository.util.Entity2DynamoDBTableSynchronizer; +import org.springframework.beans.factory.annotation.Required; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -41,53 +38,44 @@ */ public class DynamoDBRepositoryFactoryBean, S, ID extends Serializable> extends - RepositoryFactoryBeanSupport - implements - ApplicationContextAware { - - private DynamoDBMapperConfig dynamoDBMapperConfig; - - private AmazonDynamoDB amazonDynamoDB; + RepositoryFactoryBeanSupport { private DynamoDBOperations dynamoDBOperations; - - private ApplicationContext applicationContext; + private Entity2DynamoDBTableSynchronizer tableSynchronizer; + private DynamoDBMappingContextProcessor dynamoDBMappingContextProcessor; public DynamoDBRepositoryFactoryBean(Class repositoryInterface) { super(repositoryInterface); } - public void setAmazonDynamoDB(AmazonDynamoDB amazonDynamoDB) { - this.amazonDynamoDB = amazonDynamoDB; - } - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; + protected RepositoryFactorySupport createRepositoryFactory() { + assert dynamoDBOperations != null; + assert tableSynchronizer != null; + assert dynamoDBMappingContextProcessor != null; + DynamoDBRepositoryFactory dynamoDBRepositoryFactory = new DynamoDBRepositoryFactory(dynamoDBOperations); + dynamoDBRepositoryFactory.addRepositoryProxyPostProcessor(tableSynchronizer); + dynamoDBRepositoryFactory.addRepositoryProxyPostProcessor(dynamoDBMappingContextProcessor); + return dynamoDBRepositoryFactory; } - @Override - protected RepositoryFactorySupport createRepositoryFactory() { - if (dynamoDBOperations == null) { - /** - * The ApplicationContextAware within DynamoDBTemplate is not executed as - * DynamoDBTemplate is not initialized as a bean - */ - DynamoDBTemplate dynamoDBTemplate = new DynamoDBTemplate(amazonDynamoDB, dynamoDBMapperConfig); - dynamoDBTemplate.setApplicationContext(applicationContext); - dynamoDBOperations = dynamoDBTemplate; - } - return new DynamoDBRepositoryFactory(dynamoDBOperations); + @Required + public void setDynamoDBMappingContextProcessor( + DynamoDBMappingContextProcessor dynamoDBMappingContextProcessor) { + this.dynamoDBMappingContextProcessor = dynamoDBMappingContextProcessor; } - public void setDynamoDBMapperConfig(DynamoDBMapperConfig dynamoDBMapperConfig) { - this.dynamoDBMapperConfig = dynamoDBMapperConfig; + @Required + public void setEntity2DynamoDBTableSynchronizer(Entity2DynamoDBTableSynchronizer tableSynchronizer) { + this.tableSynchronizer = tableSynchronizer; } + @Required public void setDynamoDBOperations(DynamoDBOperations dynamoDBOperations) { this.dynamoDBOperations = dynamoDBOperations; } + @Required public void setDynamoDBMappingContext(DynamoDBMappingContext dynamoDBMappingContext) { setMappingContext(dynamoDBMappingContext); } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/SimpleDynamoDBCrudRepository.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/SimpleDynamoDBCrudRepository.java index be337ab4..05dfa0cd 100644 --- a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/SimpleDynamoDBCrudRepository.java +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/support/SimpleDynamoDBCrudRepository.java @@ -24,6 +24,7 @@ import org.socialsignin.spring.data.dynamodb.utils.ExceptionHandler; import org.socialsignin.spring.data.dynamodb.utils.SortHandler; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.lang.NonNull; import org.springframework.util.Assert; import java.util.Collections; @@ -93,7 +94,7 @@ public List findAllById(Iterable ids) { AtomicInteger idx = new AtomicInteger(); List keyPairs = StreamSupport.stream(ids.spliterator(), false).map(id -> { - Assert.notNull(id, "The given id at position " + idx.getAndIncrement() + " must not be null!"); + Assert.notNull(id, "The given id at position " + idx.getAndIncrement() + " must not be null!"); if (entityInformation.isRangeKeyAware()) { return new KeyPair().withHashKey(entityInformation.getHashKey(id)) @@ -202,4 +203,8 @@ public void deleteAll() { dynamoDBOperations.batchDelete(findAll()); } + @NonNull + public DynamoDBEntityInformation getEntityInformation() { + return this.entityInformation; + } } diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/DynamoDBMappingContextProcessor.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/DynamoDBMappingContextProcessor.java new file mode 100644 index 00000000..8800cc03 --- /dev/null +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/DynamoDBMappingContextProcessor.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.repository.util; + +import org.socialsignin.spring.data.dynamodb.mapping.DynamoDBMappingContext; +import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBEntityInformation; +import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; + +public class DynamoDBMappingContextProcessor extends EntityInformationProxyPostProcessor + implements + RepositoryProxyPostProcessor { + + private final DynamoDBMappingContext context; + + public DynamoDBMappingContextProcessor(DynamoDBMappingContext context) { + this.context = context; + } + + @Override + protected void registeredEntity(DynamoDBEntityInformation entityInformation) { + // register entities + context.getPersistentEntity(entityInformation.getJavaType()); + } + +} diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DDL.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DDL.java new file mode 100644 index 00000000..9795aa3c --- /dev/null +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DDL.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.repository.util; + +/** + * Configuration key is {@code spring.data.dynamodb.entity2ddl.auto} Inspired by + * Hibernate's hbm2ddl + * + * @see Hibernate + * User Guide + */ +public enum Entity2DDL { + /** No action will be performed. */ + NONE("none"), + + /** Database creation will be generated on ApplicationContext startup. */ + CREATE_ONLY("create-only"), + + /** Database dropping will be generated on ApplicationContext shutdown. */ + DROP("drop"), + + /** + * Database dropping will be generated followed by database creation on + * ApplicationContext startup. + */ + CREATE("create"), + + /** + * Drop the schema and recreate it on ApplicationContext startup. Additionally, + * drop the schema on ApplicationContext shutdown. + */ + CREATE_DROP("create-drop"), + + /** Validate the database schema */ + VALIDATE("validate"); + + private final String configurationValue; + + Entity2DDL(String configurationValue) { + this.configurationValue = configurationValue; + } + + public String getConfigurationValue() { + return this.configurationValue; + } + + /** + * Use this in place of valueOf. + * + * @param value + * real value + * @return Entity2DDL corresponding to the value + * + * @throws IllegalArgumentException + * If the specified value does not map to one of the known values in + * this enum. + */ + public static Entity2DDL fromValue(String value) { + for (Entity2DDL resolvedConfig : Entity2DDL.values()) { + if (resolvedConfig.configurationValue.equals(value)) { + return resolvedConfig; + } + } + throw new IllegalArgumentException(value + " is not a valid configuration value!"); + } +} \ No newline at end of file diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DynamoDBTableSynchronizer.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DynamoDBTableSynchronizer.java new file mode 100644 index 00000000..51595528 --- /dev/null +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DynamoDBTableSynchronizer.java @@ -0,0 +1,229 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.repository.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBEntityInformation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ApplicationContextEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.ContextStoppedEvent; +import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; +import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; +import com.amazonaws.services.dynamodbv2.model.Projection; +import com.amazonaws.services.dynamodbv2.model.ProjectionType; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.TableDescription; +import com.amazonaws.services.dynamodbv2.util.TableUtils; +import com.amazonaws.services.dynamodbv2.util.TableUtils.TableNeverTransitionedToStateException; + +/** + * This is the base class for all classes performing the validation or + * auto-creation of tables based on the entity classes. + * + * //TODO: It would be nice if the checks would run in parallel via a + * TaskScheduler (if available) + * + * @see Entity2DDL + */ +public class Entity2DynamoDBTableSynchronizer extends EntityInformationProxyPostProcessor + implements + RepositoryProxyPostProcessor, + ApplicationListener { + private static final Logger LOGGER = LoggerFactory.getLogger(Entity2DynamoDBTableSynchronizer.class); + + private static final String CONFIGURATION_KEY_entity2ddl_auto = "${spring.data.dynamodb.entity2ddl.auto:none}"; + private static final String CONFIGURATION_KEY_entity2ddl_gsiProjectionType = "${spring.data.dynamodb.entity2ddl.gsiProjectionType:ALL}"; + private static final String CONFIGURATION_KEY_entity2ddl_readCapacity = "${spring.data.dynamodb.entity2ddl.readCapacity:10}"; + private static final String CONFIGURATION_KEY_entity2ddl_writeCapacity = "${spring.data.dynamodb.entity2ddl.writeCapacity:1}"; + + private final AmazonDynamoDB amazonDynamoDB; + private final DynamoDBMapper mapper; + + private final Entity2DDL mode; + private final ProjectionType gsiProjectionType; + private final ProvisionedThroughput pt; + + private final Collection> registeredEntities = new ArrayList<>(); + + public Entity2DynamoDBTableSynchronizer(AmazonDynamoDB amazonDynamoDB, DynamoDBMapper mapper, Entity2DDL mode) { + this(amazonDynamoDB, mapper, mode.getConfigurationValue(), ProjectionType.ALL.name(), 10L, 10L); + } + + @Autowired + public Entity2DynamoDBTableSynchronizer(AmazonDynamoDB amazonDynamoDB, DynamoDBMapper mapper, + @Value(CONFIGURATION_KEY_entity2ddl_auto) String mode, + @Value(CONFIGURATION_KEY_entity2ddl_gsiProjectionType) String gsiProjectionType, + @Value(CONFIGURATION_KEY_entity2ddl_readCapacity) long readCapacity, + @Value(CONFIGURATION_KEY_entity2ddl_writeCapacity) long writeCapacity) { + this.amazonDynamoDB = amazonDynamoDB; + this.mapper = mapper; + + this.mode = Entity2DDL.fromValue(mode); + this.pt = new ProvisionedThroughput(readCapacity, writeCapacity); + this.gsiProjectionType = ProjectionType.fromValue(gsiProjectionType); + } + + @Override + protected void registeredEntity(DynamoDBEntityInformation entityInformation) { + this.registeredEntities.add(entityInformation); + } + + @Override + public void onApplicationEvent(ApplicationContextEvent event) { + LOGGER.info("Checking repository classes with DynamoDB tables {} for {}", + registeredEntities.stream().map(e -> e.getDynamoDBTableName()).collect(Collectors.joining(", ")), + event.getClass().getSimpleName()); + + for (DynamoDBEntityInformation entityInformation : registeredEntities) { + + try { + synchronize(entityInformation, event); + } catch (TableNeverTransitionedToStateException | InterruptedException e) { + throw new RuntimeException("Could not perform Entity2DDL operation " + mode + " on " + + entityInformation.getDynamoDBTableName(), e); + } + } + } + + protected void synchronize(DynamoDBEntityInformation entityInformation, ApplicationContextEvent event) + throws TableNeverTransitionedToStateException, InterruptedException { + + if (event instanceof ContextRefreshedEvent) { + switch (mode) { + case CREATE_DROP : + case CREATE : + performDrop(entityInformation); + // TODO implement wait for deletion + case CREATE_ONLY : + performCreate(entityInformation); + break; + case VALIDATE : + performValidate(entityInformation); + break; + case DROP : + case NONE : + default : + LOGGER.debug("No auto table DDL performed on start"); + break; + } + } else if (event instanceof ContextStoppedEvent) { + switch (mode) { + case CREATE_DROP : + case DROP : + performDrop(entityInformation); + performCreate(entityInformation); + break; + + case CREATE : + case VALIDATE : + case NONE : + default : + LOGGER.debug("No auto table DDL performed on stop"); + break; + } + } else { + LOGGER.trace("Ignored ApplicationContextEvent: {}", event); + } + + } + + private boolean performCreate(DynamoDBEntityInformation entityInformation) + throws TableNeverTransitionedToStateException, InterruptedException { + Class domainType = entityInformation.getJavaType(); + + CreateTableRequest ctr = mapper.generateCreateTableRequest(domainType); + LOGGER.trace("Creating table {} for entity {}", ctr.getTableName(), domainType); + ctr.setProvisionedThroughput(pt); + + if (ctr.getGlobalSecondaryIndexes() != null) { + ctr.getGlobalSecondaryIndexes().forEach(gsi -> { + gsi.setProjection(new Projection().withProjectionType(gsiProjectionType)); + gsi.setProvisionedThroughput(pt); + }); + } + + boolean result = TableUtils.createTableIfNotExists(amazonDynamoDB, ctr); + if (result) { + TableUtils.waitUntilActive(amazonDynamoDB, ctr.getTableName()); + LOGGER.debug("Created table {} for entity {}", ctr.getTableName(), domainType); + } + return result; + } + + private boolean performDrop(DynamoDBEntityInformation entityInformation) { + Class domainType = entityInformation.getJavaType(); + + DeleteTableRequest dtr = mapper.generateDeleteTableRequest(domainType); + LOGGER.trace("Dropping table {} for entity {}", dtr.getTableName(), domainType); + + boolean result = TableUtils.deleteTableIfExists(amazonDynamoDB, dtr); + if (result) { + LOGGER.debug("Deleted table {} for entity {}", dtr.getTableName(), domainType); + } + + return result; + } + + /** + * @param entityInformation + * The entity to check for it's table + * @throws IllegalStateException + * is thrown if the existing table doesn't match the entity's + * annotation + */ + private DescribeTableResult performValidate(DynamoDBEntityInformation entityInformation) + throws IllegalStateException { + Class domainType = entityInformation.getJavaType(); + + CreateTableRequest expected = mapper.generateCreateTableRequest(domainType); + DescribeTableResult result = amazonDynamoDB.describeTable(expected.getTableName()); + TableDescription actual = result.getTable(); + + if (!expected.getKeySchema().equals(actual.getKeySchema())) { + throw new IllegalStateException("KeySchema is not as expected. Expected: <" + expected.getKeySchema() + + "> but found <" + actual.getKeySchema() + ">"); + } + LOGGER.debug("KeySchema is valid"); + + if (expected.getGlobalSecondaryIndexes() != null) { + if (!Arrays.deepEquals(expected.getGlobalSecondaryIndexes().toArray(), + actual.getGlobalSecondaryIndexes().toArray())) { + throw new IllegalStateException("Global Secondary Indexes are not as expected. Expected: <" + + expected.getGlobalSecondaryIndexes() + "> but found <" + actual.getGlobalSecondaryIndexes() + + ">"); + } + } + LOGGER.debug("Global Secondary Indexes are valid"); + + LOGGER.info("Validated table {} for entity{}", expected.getTableName(), domainType); + return result; + } + +} diff --git a/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/EntityInformationProxyPostProcessor.java b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/EntityInformationProxyPostProcessor.java new file mode 100644 index 00000000..19f75dba --- /dev/null +++ b/src/main/java/org/socialsignin/spring/data/dynamodb/repository/util/EntityInformationProxyPostProcessor.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.repository.util; + +import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBEntityInformation; +import org.socialsignin.spring.data.dynamodb.repository.support.SimpleDynamoDBCrudRepository; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; + +public abstract class EntityInformationProxyPostProcessor implements RepositoryProxyPostProcessor { + + protected abstract void registeredEntity(DynamoDBEntityInformation entityInformation); + + @Override + public final void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) { + try { + TargetSource targetSource = factory.getTargetSource(); + // assert + // targetSource.getTargetClass().equals(SimpleDynamoDBCrudRepository.class); + + @SuppressWarnings("unchecked") + SimpleDynamoDBCrudRepository target = SimpleDynamoDBCrudRepository.class + .cast(targetSource.getTarget()); + + assert target != null; + DynamoDBEntityInformation entityInformation = target.getEntityInformation(); + registeredEntity(entityInformation); + + } catch (Exception e) { + throw new RuntimeException("Could not extract SimpleDynamoDBCrudRepository from " + factory, e); + } + } + +} diff --git a/src/spotbugs-exclude.xml b/src/spotbugs-exclude.xml new file mode 100644 index 00000000..a014e81e --- /dev/null +++ b/src/spotbugs-exclude.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/config/AuditingViaJavaConfigRepositoriesIT.java b/src/test/java/org/socialsignin/spring/data/dynamodb/config/AuditingViaJavaConfigRepositoriesIT.java index fab032b0..b1ead7fb 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/config/AuditingViaJavaConfigRepositoriesIT.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/config/AuditingViaJavaConfigRepositoriesIT.java @@ -15,34 +15,22 @@ */ package org.socialsignin.spring.data.dynamodb.config; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; - -import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; -import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; -import org.mockito.quality.Strictness; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.socialsignin.spring.data.dynamodb.domain.sample.AuditableUser; import org.socialsignin.spring.data.dynamodb.domain.sample.AuditableUserRepository; -import org.socialsignin.spring.data.dynamodb.mapping.DynamoDBMappingContext; import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories; import org.socialsignin.spring.data.dynamodb.utils.DynamoDBLocalResource; -import org.socialsignin.spring.data.dynamodb.utils.TableCreationListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.AuditorAware; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.Optional; @@ -52,7 +40,6 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.doReturn; -import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS; /** * Integration tests for auditing via Java config. @@ -61,12 +48,8 @@ */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {DynamoDBLocalResource.class, AuditingViaJavaConfigRepositoriesIT.TestAppConfig.class}) -@TestExecutionListeners(listeners = TableCreationListener.class, mergeMode = MERGE_WITH_DEFAULTS) +@TestPropertySource(properties = {"spring.data.dynamodb.entity2ddl.auto=create"}) public class AuditingViaJavaConfigRepositoriesIT { - @Rule - public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.LENIENT); - @Mock - private static AuditorAware auditorProviderClass; private static final Logger LOGGER = LoggerFactory.getLogger(AuditingViaJavaConfigRepositoriesIT.class); @@ -78,18 +61,9 @@ public static class TestAppConfig { @SuppressWarnings("unchecked") @Bean(name = "auditorProvider") public AuditorAware auditorProvider() { - LOGGER.info("auditorProvider"); + LOGGER.info("mocked auditorProvider provided"); return Mockito.mock(AuditorAware.class); } - - @Bean - public DynamoDBMappingContext dynamoDBMappingContext() { - DynamoDBMappingContext mappingContext = new DynamoDBMappingContext(); - // Register entity - // TODO but this shouldn't be nessassary?! - mappingContext.getPersistentEntity(AuditableUser.class); - return mappingContext; - } } @Autowired @@ -100,16 +74,8 @@ public DynamoDBMappingContext dynamoDBMappingContext() { AuditableUser auditor; - @Autowired - private AmazonDynamoDB ddb; - @Before public void setUp() throws InterruptedException { - CreateTableRequest ctr = new DynamoDBMapper(ddb).generateCreateTableRequest(AuditableUser.class); - ctr.withProvisionedThroughput(new ProvisionedThroughput(10L, 10L)); - ddb.createTable(ctr); - // Thread.sleep(5 * 1000); - this.auditor = auditableUserRepository.save(new AuditableUser("auditor")); assertThat(this.auditor, is(notNullValue())); diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/core/CustomerHistoryIT.java b/src/test/java/org/socialsignin/spring/data/dynamodb/core/CustomerHistoryTest.java similarity index 79% rename from src/test/java/org/socialsignin/spring/data/dynamodb/core/CustomerHistoryIT.java rename to src/test/java/org/socialsignin/spring/data/dynamodb/core/CustomerHistoryTest.java index 86ae7f93..1147682a 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/core/CustomerHistoryIT.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/core/CustomerHistoryTest.java @@ -20,20 +20,22 @@ import org.socialsignin.spring.data.dynamodb.domain.sample.CustomerHistory; import org.socialsignin.spring.data.dynamodb.domain.sample.CustomerHistoryRepository; import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories; -import org.socialsignin.spring.data.dynamodb.utils.DynamoDBResource; +import org.socialsignin.spring.data.dynamodb.utils.DynamoDBLocalResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.MethodMode; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import static org.junit.Assert.assertEquals; -import org.junit.Ignore; - @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {CustomerHistoryIT.TestAppConfig.class, DynamoDBResource.class}) -@Ignore -public class CustomerHistoryIT { +@DirtiesContext(methodMode = MethodMode.BEFORE_METHOD) +@ContextConfiguration(classes = {CustomerHistoryTest.TestAppConfig.class, DynamoDBLocalResource.class}) +@TestPropertySource(properties = {"spring.data.dynamodb.entity2ddl.auto=create"}) +public class CustomerHistoryTest { @Configuration @EnableDynamoDBRepositories(basePackages = "org.socialsignin.spring.data.dynamodb.domain.sample") diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplateIT.java b/src/test/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplateIT.java index f5f24ec4..91499403 100755 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplateIT.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplateIT.java @@ -16,14 +16,17 @@ package org.socialsignin.spring.data.dynamodb.core; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.socialsignin.spring.data.dynamodb.domain.sample.User; -import org.socialsignin.spring.data.dynamodb.utils.DynamoDBResource; +import org.socialsignin.spring.data.dynamodb.utils.DynamoDBLocalResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.UUID; @@ -32,17 +35,21 @@ * Integration test that interacts with DynamoDB Local instance. */ @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {DynamoDBResource.class}) -@Ignore +@ContextConfiguration(classes = {DynamoDBLocalResource.class}) +@TestPropertySource(properties = {"spring.data.dynamodb.entity2ddl.auto=create"}) public class DynamoDBTemplateIT { @Autowired private AmazonDynamoDB amazonDynamoDB; private DynamoDBTemplate dynamoDBTemplate; + @Autowired + private DynamoDBMapper mapper; + @Autowired + private DynamoDBMapperConfig mapperConfig; @Before public void setUp() { - this.dynamoDBTemplate = new DynamoDBTemplate(amazonDynamoDB); + this.dynamoDBTemplate = new DynamoDBTemplate(amazonDynamoDB, mapper, mapperConfig); } @Test diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplateTest.java b/src/test/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplateTest.java index cca003a2..e25ed1e7 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplateTest.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/core/DynamoDBTemplateTest.java @@ -18,7 +18,6 @@ import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; -import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.TableNameOverride; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBQueryExpression; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBScanExpression; import org.junit.Assert; @@ -32,14 +31,13 @@ import org.socialsignin.spring.data.dynamodb.domain.sample.Playlist; import org.socialsignin.spring.data.dynamodb.domain.sample.User; import org.springframework.context.ApplicationContext; -import org.springframework.test.util.ReflectionTestUtils; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -52,6 +50,8 @@ public class DynamoDBTemplateTest { @Mock private DynamoDBMapper dynamoDBMapper; @Mock + private DynamoDBMapperConfig dynamoDBMapperConfig; + @Mock private AmazonDynamoDB dynamoDB; @Mock private ApplicationContext applicationContext; @@ -62,30 +62,43 @@ public class DynamoDBTemplateTest { @Before public void setUp() { - this.dynamoDBTemplate = new DynamoDBTemplate(dynamoDB, dynamoDBMapper); + this.dynamoDBTemplate = new DynamoDBTemplate(dynamoDB, dynamoDBMapper, dynamoDBMapperConfig); this.dynamoDBTemplate.setApplicationContext(applicationContext); - } - - @Test - public void testConstructorMandatory() { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("must not be null!"); - new DynamoDBTemplate(null); - } - - @Test - public void testConstructorOptionalAllNull() { - dynamoDBTemplate = new DynamoDBTemplate(dynamoDB, null, null); // check that the defaults are properly initialized - #108 String userTableName = dynamoDBTemplate.getOverriddenTableName(User.class, "UserTable"); - assertEquals("user", userTableName); + assertEquals("UserTable", userTableName); } + @Test + public void testConstructorAllNull() { + try { + dynamoDBTemplate = new DynamoDBTemplate(null, null, null); + fail("AmazonDynamoDB must not be null!"); + } catch (IllegalArgumentException iae) { + // ignored + } + + try { + dynamoDBTemplate = new DynamoDBTemplate(dynamoDB, null, null); + fail("DynamoDBMapper must not be null!"); + } catch (IllegalArgumentException iae) { + // ignored + } + try { + dynamoDBTemplate = new DynamoDBTemplate(dynamoDB, dynamoDBMapper, null); + fail("DynamoDBMapperConfig must not be null!"); + } catch (IllegalArgumentException iae) { + // ignored + } + assertTrue(true); + } + + // TODO remove and replace with postprocessor test @Test public void testConstructorOptionalPreconfiguredDynamoDBMapper() { // Introduced constructor via #91 should not fail its assert - new DynamoDBTemplate(dynamoDB, dynamoDBMapper); + this.dynamoDBTemplate = new DynamoDBTemplate(dynamoDB, dynamoDBMapper, dynamoDBMapperConfig); assertTrue("The constructor should not fail with an assert error", true); } @@ -138,64 +151,6 @@ public void testCountScan() { verify(dynamoDBMapper).count(User.class, scan); } - @Test - public void testGetOverriddenTableName_WithTableNameResolver_defaultConfig() { - final String overridenTableName = "someOtherTableName"; - - DynamoDBMapperConfig.Builder builder = DynamoDBMapperConfig.builder(); - // Inject the table name overrider bean - builder.setTableNameOverride(new TableNameOverride(overridenTableName)); - - DynamoDBMapperConfig config = new DynamoDBMapperConfig(DynamoDBMapperConfig.DEFAULT, builder.build()); - DynamoDBTemplate tmpl = new DynamoDBTemplate(dynamoDB, config, dynamoDBMapper); - - String overriddenTableName = tmpl.getOverriddenTableName(User.class, "someTableName"); - assertEquals(overridenTableName, overriddenTableName); - - DynamoDBMapperConfig effectiveConfig = (DynamoDBMapperConfig) ReflectionTestUtils.getField(tmpl, - "dynamoDBMapperConfig"); - assertDynamoDBMapperConfigCompletness(tmpl); - } - - @Test - public void testGetOverriddenTableName_WithTableNameResolver_defaultBuilder() { - final String overridenTableName = "someOtherTableName"; - - DynamoDBMapperConfig.Builder builder = new DynamoDBMapperConfig.Builder(); - // Inject the table name overrider bean - builder.setTableNameOverride(new TableNameOverride(overridenTableName)); - - DynamoDBTemplate tmpl = new DynamoDBTemplate(dynamoDB, builder.build(), dynamoDBMapper); - - String overriddenTableName = tmpl.getOverriddenTableName(User.class, "someTableName"); - assertEquals(overridenTableName, overriddenTableName); - assertDynamoDBMapperConfigCompletness(tmpl); - } - - @Test - public void testGetOverriddenTableName_WithTableNameResolver_emptyBuilder() { - final String overridenTableName = "someOtherTableName"; - - DynamoDBMapperConfig.Builder builder = DynamoDBMapperConfig.builder(); - // Inject the table name overrider bean - builder.setTableNameOverride(new TableNameOverride(overridenTableName)); - - DynamoDBTemplate tmpl = new DynamoDBTemplate(dynamoDB, builder.build(), dynamoDBMapper); - - String overriddenTableName = tmpl.getOverriddenTableName(User.class, "someTableName"); - assertEquals(overridenTableName, overriddenTableName); - assertDynamoDBMapperConfigCompletness(tmpl); - } - - private void assertDynamoDBMapperConfigCompletness(DynamoDBTemplate tmpl) { - DynamoDBMapperConfig effectiveConfig = (DynamoDBMapperConfig) ReflectionTestUtils.getField(tmpl, - "dynamoDBMapperConfig"); - - assertNotNull(effectiveConfig); - assertNotNull(effectiveConfig.getConversionSchema()); - assertNotNull(effectiveConfig.getTypeConverterFactory()); - } - @Test public void testLoadByHashKey_WhenDynamoDBMapperReturnsNull() { User user = dynamoDBTemplate.load(User.class, "someHashKey"); diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/core/FeedUserIT.java b/src/test/java/org/socialsignin/spring/data/dynamodb/core/FeedUserIT.java index 2837c35a..bec3d373 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/core/FeedUserIT.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/core/FeedUserIT.java @@ -15,23 +15,23 @@ */ package org.socialsignin.spring.data.dynamodb.core; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.socialsignin.spring.data.dynamodb.domain.sample.FeedUserRepository; import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories; -import org.socialsignin.spring.data.dynamodb.utils.DynamoDBResource; +import org.socialsignin.spring.data.dynamodb.utils.DynamoDBLocalResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {FeedUserIT.TestAppConfig.class, DynamoDBResource.class}) -@Ignore +@ContextConfiguration(classes = {FeedUserIT.TestAppConfig.class, DynamoDBLocalResource.class}) +@TestPropertySource(properties = {"spring.data.dynamodb.entity2ddl.auto=create"}) public class FeedUserIT { @Configuration diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/core/SortPageableIT.java b/src/test/java/org/socialsignin/spring/data/dynamodb/core/SortPageableIT.java new file mode 100644 index 00000000..4f1ef6a8 --- /dev/null +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/core/SortPageableIT.java @@ -0,0 +1,80 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.time.LocalDateTime; +import java.util.Random; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.socialsignin.spring.data.dynamodb.domain.sample.Feed; +import org.socialsignin.spring.data.dynamodb.domain.sample.FeedPagingRepository; +import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories; +import org.socialsignin.spring.data.dynamodb.utils.DynamoDBLocalResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {SortPageableIT.TestAppConfig.class, DynamoDBLocalResource.class}) +@TestPropertySource(properties = {"spring.data.dynamodb.entity2ddl.auto=create"}) +public class SortPageableIT { + private final Random r = new Random(); + + @Configuration + @EnableDynamoDBRepositories(basePackages = "org.socialsignin.spring.data.dynamodb.domain.sample") + public static class TestAppConfig { + } + + @Autowired + FeedPagingRepository feedPagingRepository; + + private Feed createFeed(String message) { + Feed retValue = new Feed(); + retValue.setUserIdx(r.nextInt()); + retValue.setPaymentType(r.nextInt()); + retValue.setMessage(message); + retValue.setRegDate(LocalDateTime.now()); + return retValue; + } + + @Test + public void feed_test() { + feedPagingRepository.save(createFeed("not yet me")); + feedPagingRepository.save(createFeed("me")); + feedPagingRepository.save(createFeed("not me")); + feedPagingRepository.save(createFeed("me")); + feedPagingRepository.save(createFeed("also not me")); + + PageRequest pageable = PageRequest.of(0, 10); + + Page actuals = feedPagingRepository.findAllByMessageOrderByRegDateDesc("me", pageable); + assertEquals(2, actuals.getTotalElements()); + + for (Feed actual : actuals) { + assertNotEquals(0, actual.getPaymentType()); + assertNotEquals(0, actual.getUserIdx()); + } + + } +} diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CRUDOperationsIT.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CRUDOperationsIT.java index 132d6fba..9fe1db0f 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CRUDOperationsIT.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CRUDOperationsIT.java @@ -15,11 +15,7 @@ */ package org.socialsignin.spring.data.dynamodb.domain.sample; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; -import com.amazonaws.services.dynamodbv2.document.Expected; - import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -27,8 +23,6 @@ import org.junit.runner.RunWith; import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories; import org.socialsignin.spring.data.dynamodb.utils.DynamoDBLocalResource; -import org.socialsignin.spring.data.dynamodb.utils.TableCreationListener; -import org.socialsignin.spring.data.dynamodb.utils.TableCreationListener.DynamoDBCreateTable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.dao.EmptyResultDataAccessException; @@ -36,13 +30,12 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; @@ -55,12 +48,10 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {DynamoDBLocalResource.class, CRUDOperationsIT.TestAppConfig.class}) -@TestExecutionListeners(listeners = TableCreationListener.class, mergeMode = MERGE_WITH_DEFAULTS) -@DynamoDBCreateTable(entityClasses = {User.class, Playlist.class}) +@TestPropertySource(properties = {"spring.data.dynamodb.entity2ddl.auto=create"}) public class CRUDOperationsIT { @Rule diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CustomerDocumentTest.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CustomerDocumentTest.java index 24cb504e..78fffe0e 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CustomerDocumentTest.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/CustomerDocumentTest.java @@ -19,20 +19,17 @@ import org.junit.runner.RunWith; import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories; import org.socialsignin.spring.data.dynamodb.utils.DynamoDBLocalResource; -import org.socialsignin.spring.data.dynamodb.utils.TableCreationListener; -import org.socialsignin.spring.data.dynamodb.utils.TableCreationListener.DynamoDBCreateTable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS; - @RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext @ContextConfiguration(classes = {DynamoDBLocalResource.class, CustomerDocumentTest.TestAppConfig.class}) -@TestExecutionListeners(listeners = TableCreationListener.class, mergeMode = MERGE_WITH_DEFAULTS) -@DynamoDBCreateTable(entityClasses = {CustomerDocument.class}) +@TestPropertySource(properties = {"spring.data.dynamodb.entity2ddl.auto=create"}) public class CustomerDocumentTest { @Configuration diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/Feed.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/Feed.java new file mode 100644 index 00000000..53074f75 --- /dev/null +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/Feed.java @@ -0,0 +1,101 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.domain.sample; + +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAutoGeneratedKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexHashKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexRangeKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverted; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverter; +import java.time.LocalDateTime; + +@DynamoDBTable(tableName = "gz_feed") +public class Feed { + private String idx; + private int userIdx; + private String message; + private int paymentType; + private LocalDateTime regDate; + + @DynamoDBHashKey + @DynamoDBAutoGeneratedKey + public String getIdx() { + return idx; + } + + @DynamoDBAttribute + public int getUserIdx() { + return userIdx; + } + + @DynamoDBAttribute + @DynamoDBIndexHashKey(globalSecondaryIndexName = "aaa") + public String getMessage() { + return message; + } + + // @DynamoDBIndexRangeKey(attributeName = "PaymentType", + // globalSecondaryIndexName = "aaa") + @DynamoDBAttribute + public int getPaymentType() { + return paymentType; + } + + @DynamoDBTypeConverted(converter = LocalDateTimeConverter.class) + @DynamoDBAttribute + @DynamoDBIndexRangeKey(globalSecondaryIndexName = "aaa") + public LocalDateTime getRegDate() { + return regDate; + } + + public void setIdx(String idx) { + this.idx = idx; + } + + public void setUserIdx(int userIdx) { + this.userIdx = userIdx; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setPaymentType(int paymentType) { + this.paymentType = paymentType; + } + + public void setRegDate(LocalDateTime regDate) { + this.regDate = regDate; + } + + static public class LocalDateTimeConverter implements DynamoDBTypeConverter { + + @Override + public String convert(final LocalDateTime time) { + + return time.toString(); + } + + @Override + public LocalDateTime unconvert(final String stringValue) { + + return LocalDateTime.parse(stringValue); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/FeedPagingRepository.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/FeedPagingRepository.java new file mode 100644 index 00000000..6cd0e75d --- /dev/null +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/FeedPagingRepository.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.domain.sample; + +import org.socialsignin.spring.data.dynamodb.repository.DynamoDBPagingAndSortingRepository; +import org.socialsignin.spring.data.dynamodb.repository.EnableScan; +import org.socialsignin.spring.data.dynamodb.repository.EnableScanCount; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +@EnableScan +@EnableScanCount +public interface FeedPagingRepository extends DynamoDBPagingAndSortingRepository { + Page findAllByMessageOrderByRegDateDesc(String message, Pageable pageable); +} \ No newline at end of file diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/GlobalSecondaryIndexWithRangeKeyIT.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/GlobalSecondaryIndexWithRangeKeyIT.java index fa1f3b49..27a48d66 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/GlobalSecondaryIndexWithRangeKeyIT.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/GlobalSecondaryIndexWithRangeKeyIT.java @@ -15,14 +15,14 @@ */ package org.socialsignin.spring.data.dynamodb.domain.sample; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories; -import org.socialsignin.spring.data.dynamodb.utils.DynamoDBResource; +import org.socialsignin.spring.data.dynamodb.utils.DynamoDBLocalResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.Calendar; @@ -38,8 +38,8 @@ * Shows the usage of Hash+Range key combinations with global secondary indexes. */ @RunWith(SpringJUnit4ClassRunner.class) -@Ignore -@ContextConfiguration(classes = {DynamoDBResource.class, GlobalSecondaryIndexWithRangeKeyIT.TestAppConfig.class}) +@ContextConfiguration(classes = {DynamoDBLocalResource.class, GlobalSecondaryIndexWithRangeKeyIT.TestAppConfig.class}) +@TestPropertySource(properties = {"spring.data.dynamodb.entity2ddl.auto=create"}) public class GlobalSecondaryIndexWithRangeKeyIT { @Configuration diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/Jdk8IT.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/Jdk8IT.java index aeb1626f..99ba2160 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/Jdk8IT.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/Jdk8IT.java @@ -15,14 +15,14 @@ */ package org.socialsignin.spring.data.dynamodb.domain.sample; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.socialsignin.spring.data.dynamodb.repository.config.EnableDynamoDBRepositories; -import org.socialsignin.spring.data.dynamodb.utils.DynamoDBResource; +import org.socialsignin.spring.data.dynamodb.utils.DynamoDBLocalResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.time.Instant; @@ -30,7 +30,10 @@ import java.util.List; import java.util.Optional; import java.util.UUID; - +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -42,8 +45,8 @@ * github.com/spring-projects/spring-data-examples/master/jpa/java8 */ @RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {DynamoDBResource.class, Jdk8IT.TestAppConfig.class}) -@Ignore +@ContextConfiguration(classes = {DynamoDBLocalResource.class, Jdk8IT.TestAppConfig.class}) +@TestPropertySource(properties = {"spring.data.dynamodb.entity2ddl.auto=create"}) public class Jdk8IT { @Configuration @@ -76,6 +79,24 @@ public void testOptionalKey() { assertEquals(joinDate, result.get().getJoinDate()); } + @Test + public void testFuture() throws InterruptedException, ExecutionException, TimeoutException { + User user = new User(); + user.setName("testFuture"); + user.setPostCode("postCode"); + user = userRepository.save(user); + + User actual; + // Future result + actual = userRepository.findByName("testFuture").get(); + // assertNotNull(result); + // User actual = result.get(1, TimeUnit.MINUTES); + + assertNotNull(actual); + assertNotNull(actual.getId()); + assertEquals("postCode", actual.getPostCode()); + } + @Test public void testOptionalFilter() { final Date joinDate = new Date(2000); diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/User.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/User.java index ad77d903..e23daacf 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/User.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/User.java @@ -15,7 +15,6 @@ */ package org.socialsignin.spring.data.dynamodb.domain.sample; -import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAutoGeneratedDefault; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAutoGeneratedKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexHashKey; diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/UserRepository.java b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/UserRepository.java index 086d2274..c30018fa 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/UserRepository.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/domain/sample/UserRepository.java @@ -22,6 +22,7 @@ import java.time.Instant; import java.util.List; import java.util.Optional; +import java.util.concurrent.Future; public interface UserRepository extends CrudRepository { @@ -38,6 +39,11 @@ public interface UserRepository extends CrudRepository { @EnableScan Optional findByName(String name); + @EnableScan + Future findOneByPostCode(String postCode); + @EnableScan + User findFirstByPostCode(String postCode); + T save(T entity); @EnableScan diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBMapperConfigFactoryTest.java b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBMapperConfigFactoryTest.java new file mode 100644 index 00000000..e168dad2 --- /dev/null +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/config/DynamoDBMapperConfigFactoryTest.java @@ -0,0 +1,97 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.repository.config; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.TableNameOverride; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +@RunWith(MockitoJUnitRunner.class) +public class DynamoDBMapperConfigFactoryTest { + + @Mock + private DynamoDBMapper dynamoDBMapper; + @Mock + private DynamoDBMapperConfig dynamoDBMapperConfig; + @Mock + private AmazonDynamoDB dynamoDB; + + DynamoDBMapperConfigFactory underTest; + + @Before + public void setUp() throws Exception { + underTest = new DynamoDBMapperConfigFactory(); + } + + @Test + public void testGetOverriddenTableName_WithTableNameResolver_defaultConfig() { + + DynamoDBMapperConfig actual = (DynamoDBMapperConfig) underTest + .postProcessAfterInitialization(DynamoDBMapperConfig.DEFAULT, null); + + assertSame(DynamoDBMapperConfig.DEFAULT, actual); + } + + @Test + public void testGetOverriddenTableName_WithTableNameResolver_defaultBuilder() { + final String overridenTableName = "someOtherTableName"; + + DynamoDBMapperConfig.Builder builder = new DynamoDBMapperConfig.Builder(); + // Inject the table name overrider bean + builder.setTableNameOverride(new TableNameOverride(overridenTableName)); + + DynamoDBMapperConfig actual = (DynamoDBMapperConfig) underTest.postProcessAfterInitialization(builder.build(), + null); + + String overriddenTableName = actual.getTableNameOverride().getTableName(); + assertEquals(overridenTableName, overriddenTableName); + + assertDynamoDBMapperConfigCompletness(actual); + } + + @Test + public void testGetOverriddenTableName_WithTableNameResolver_emptyBuilder() { + final String overridenTableName = "someOtherTableName"; + + DynamoDBMapperConfig.Builder builder = DynamoDBMapperConfig.builder(); + // Inject the table name overrider bean + builder.setTableNameOverride(new TableNameOverride(overridenTableName)); + + DynamoDBMapperConfig actual = (DynamoDBMapperConfig) underTest.postProcessAfterInitialization(builder.build(), + null); + + String overriddenTableName = actual.getTableNameOverride().getTableName(); + assertEquals(overridenTableName, overriddenTableName); + + assertDynamoDBMapperConfigCompletness(actual); + } + + private void assertDynamoDBMapperConfigCompletness(DynamoDBMapperConfig effectiveConfig) { + assertNotNull(effectiveConfig); + assertNotNull(effectiveConfig.getConversionSchema()); + assertNotNull(effectiveConfig.getTypeConverterFactory()); + } + +} diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactoryBeanTest.java b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactoryBeanTest.java index 4d14bf35..2aa00671 100644 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactoryBeanTest.java +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/support/DynamoDBRepositoryFactoryBeanTest.java @@ -26,6 +26,8 @@ import org.socialsignin.spring.data.dynamodb.core.DynamoDBOperations; import org.socialsignin.spring.data.dynamodb.domain.sample.User; import org.socialsignin.spring.data.dynamodb.mapping.DynamoDBMappingContext; +import org.socialsignin.spring.data.dynamodb.repository.util.DynamoDBMappingContextProcessor; +import org.socialsignin.spring.data.dynamodb.repository.util.Entity2DynamoDBTableSynchronizer; import org.springframework.context.ApplicationContext; import org.springframework.data.repository.Repository; @@ -45,6 +47,10 @@ public class DynamoDBRepositoryFactoryBeanTest { private DynamoDBMapperConfig dynamoDBMapperConfig; @Mock private AmazonDynamoDB amazonDynamoDB; + @Mock + private Entity2DynamoDBTableSynchronizer tableSynchronizer; + @Mock + private DynamoDBMappingContextProcessor dynamoDBMappingContextProcessor; private DynamoDBMappingContext dynamoDBMappingContext = new DynamoDBMappingContext(); @@ -57,9 +63,9 @@ public interface UserRepository extends Repository { @Before public void setUp() { underTest = spy(new DynamoDBRepositoryFactoryBean<>(UserRepository.class)); - underTest.setApplicationContext(applicationContext); - underTest.setDynamoDBMapperConfig(dynamoDBMapperConfig); underTest.setDynamoDBMappingContext(dynamoDBMappingContext); + underTest.setEntity2DynamoDBTableSynchronizer(tableSynchronizer); + underTest.setDynamoDBMappingContextProcessor(dynamoDBMappingContextProcessor); } @Test @@ -86,7 +92,7 @@ public void testAmazonDynamoDB() { assertTrue(true); } - underTest.setAmazonDynamoDB(amazonDynamoDB); + underTest.setDynamoDBOperations(dynamoDBOperations); underTest.afterPropertiesSet(); assertNotNull(underTest.getPersistentEntity()); diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DDLTest.java b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DDLTest.java new file mode 100644 index 00000000..a53d8667 --- /dev/null +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DDLTest.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.repository.util; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import static org.junit.Assert.assertEquals; + +public class Entity2DDLTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testFromExistingValue() { + Entity2DDL actual = Entity2DDL.fromValue(Entity2DDL.NONE.getConfigurationValue()); + + assertEquals(Entity2DDL.NONE, actual); + } + + @Test + public void testFromNotExistingValue() { + expectedException.expect(IllegalArgumentException.class); + + Entity2DDL.fromValue("doesnt exist"); + } + +} diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DynamoDBTableSynchronizerTest.java b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DynamoDBTableSynchronizerTest.java new file mode 100644 index 00000000..953fdb0b --- /dev/null +++ b/src/test/java/org/socialsignin/spring/data/dynamodb/repository/util/Entity2DynamoDBTableSynchronizerTest.java @@ -0,0 +1,162 @@ +/** + * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.socialsignin.spring.data.dynamodb.repository.util; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBEntityInformation; +import org.socialsignin.spring.data.dynamodb.repository.support.SimpleDynamoDBCrudRepository; +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.ContextStoppedEvent; +import org.springframework.data.repository.core.RepositoryInformation; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; +import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; +import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; +import com.amazonaws.services.dynamodbv2.model.TableDescription; +import com.amazonaws.services.dynamodbv2.model.TableStatus; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class Entity2DynamoDBTableSynchronizerTest { + + private Entity2DynamoDBTableSynchronizer underTest; + @Mock + private AmazonDynamoDB amazonDynamoDB; + @Mock + private DynamoDBMapper mapper; + @Mock + private ProxyFactory factory; + @Mock + private RepositoryInformation repositoryInformation; + @Mock + private ApplicationContext applicationContext; + @Mock + private SimpleDynamoDBCrudRepository repository; + @Mock + private DynamoDBEntityInformation entityInformation; + + @Before + public void setUp() throws Exception { + TargetSource targetSource = mock(TargetSource.class); + when(targetSource.getTarget()).thenReturn(repository); + when(factory.getTargetSource()).thenReturn(targetSource); + + when(repository.getEntityInformation()).thenReturn(entityInformation); + + when(entityInformation.getDynamoDBTableName()).thenReturn("tableName"); + + CreateTableRequest ctr = mock(CreateTableRequest.class); + when(mapper.generateCreateTableRequest(any())).thenReturn(ctr); + DeleteTableRequest dtr = mock(DeleteTableRequest.class); + when(mapper.generateDeleteTableRequest(any())).thenReturn(dtr); + + DescribeTableResult describeResult = mock(DescribeTableResult.class); + TableDescription description = mock(TableDescription.class); + when(description.getTableStatus()).thenReturn(TableStatus.ACTIVE.toString()); + when(describeResult.getTable()).thenReturn(description); + when(amazonDynamoDB.describeTable(any(DescribeTableRequest.class))).thenReturn(describeResult); + } + + public void setUp(Entity2DDL mode) { + underTest = new Entity2DynamoDBTableSynchronizer<>(amazonDynamoDB, mapper, mode); + underTest.postProcess(factory, repositoryInformation); + } + + public void runContextStart() { + underTest.onApplicationEvent(new ContextRefreshedEvent(applicationContext)); + } + + public void runContextStop() { + underTest.onApplicationEvent(new ContextStoppedEvent(applicationContext)); + } + + @Test + public void testUnmatchedEvent() { + setUp(Entity2DDL.NONE); + + underTest.onApplicationEvent(new ContextClosedEvent(applicationContext)); + + verify(factory).getTargetSource(); + verifyNoMoreInteractions(amazonDynamoDB, mapper, factory, repositoryInformation, applicationContext); + } + + @Test + public void testNone() { + setUp(Entity2DDL.NONE); + + runContextStart(); + + runContextStop(); + + verify(factory).getTargetSource(); + verifyNoMoreInteractions(amazonDynamoDB, mapper, factory, repositoryInformation, applicationContext); + + } + + @Test + public void testCreate() { + setUp(Entity2DDL.CREATE); + + runContextStart(); + verify(amazonDynamoDB).deleteTable(any(DeleteTableRequest.class)); + verify(amazonDynamoDB).createTable(any()); + verify(amazonDynamoDB).describeTable(any(DescribeTableRequest.class)); + + runContextStop(); + verify(amazonDynamoDB).deleteTable(any(DeleteTableRequest.class)); + verifyNoMoreInteractions(amazonDynamoDB); + } + + @Test + public void testDrop() { + setUp(Entity2DDL.DROP); + + runContextStart(); + + runContextStop(); + } + + @Test + public void testCreateOnly() { + setUp(Entity2DDL.CREATE_ONLY); + + runContextStart(); + verify(amazonDynamoDB).createTable(any()); + verify(amazonDynamoDB).describeTable(any(DescribeTableRequest.class)); + + runContextStop(); + verifyNoMoreInteractions(amazonDynamoDB); + } + + @Test + public void testCreateDrop() { + setUp(Entity2DDL.CREATE_DROP); + + runContextStart(); + + runContextStop(); + } +} diff --git a/src/test/java/org/socialsignin/spring/data/dynamodb/utils/TableCreationListener.java b/src/test/java/org/socialsignin/spring/data/dynamodb/utils/TableCreationListener.java deleted file mode 100644 index 376ae538..00000000 --- a/src/test/java/org/socialsignin/spring/data/dynamodb/utils/TableCreationListener.java +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.socialsignin.spring.data.dynamodb.utils; - -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; -import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; -import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; -import com.amazonaws.services.dynamodbv2.model.CreateTableResult; -import com.amazonaws.services.dynamodbv2.model.Projection; -import com.amazonaws.services.dynamodbv2.model.ProjectionType; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.test.context.TestContext; -import org.springframework.test.context.support.AbstractTestExecutionListener; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.util.Arrays; - -public class TableCreationListener extends AbstractTestExecutionListener { - - public static CreateTableResult createTable(AmazonDynamoDB ddb, Class domainType) { - DynamoDBMapper mapper = new DynamoDBMapper(ddb); - - ProvisionedThroughput pt = new ProvisionedThroughput(10L, 10L); - - CreateTableRequest ctr = mapper.generateCreateTableRequest(domainType); - ctr.setProvisionedThroughput(pt); - if (ctr.getGlobalSecondaryIndexes() != null) { - ctr.getGlobalSecondaryIndexes().forEach(gsi -> { - gsi.setProjection(new Projection().withProjectionType(ProjectionType.ALL)); - gsi.setProvisionedThroughput(pt); - }); - } - - CreateTableResult ctResponse = ddb.createTable(ctr); - - do { - try { - Thread.sleep(1 * 1000L); - } catch (InterruptedException e) { - throw new RuntimeException("Couldn't wait detect table " + ctr.getTableName()); - } - } while (!ddb.describeTable(ctr.getTableName()).getTable().getTableStatus().equals("ACTIVE")); - - return ctResponse; - } - - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) - @Documented - public @interface DynamoDBCreateTable { - Class[] entityClasses(); - } - - protected Class[] getEntityClasses() { - return new Class[0]; - } - - private void createExpliclitEntities(TestContext testContext) { - BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory(); - - AmazonDynamoDB ddb = bf.getBean(AmazonDynamoDB.class); - - Arrays.stream(getEntityClasses()).forEach(clazz -> createTable(ddb, clazz)); - } - - @Override - public void beforeTestClass(TestContext testContext) { - createExpliclitEntities(testContext); - createAnnotationEntities(testContext); - } - - private void createAnnotationEntities(TestContext testContext) { - - Class testClass = testContext.getTestClass(); - if (testClass.isAnnotationPresent(DynamoDBCreateTable.class)) { - - DynamoDBCreateTable createTableAnnotation = testClass.getAnnotation(DynamoDBCreateTable.class); - - AmazonDynamoDB ddb = getAmazonDynamoDB(testContext); - - for (Class entityClass : createTableAnnotation.entityClasses()) { - createTable(ddb, entityClass); - } - } - } - - @Override - public void afterTestClass(TestContext testContext) throws Exception { - getAmazonDynamoDB(testContext).shutdown(); - } - - private BeanFactory getBeanFactory(TestContext testContext) { - return testContext.getApplicationContext().getAutowireCapableBeanFactory(); - } - - private AmazonDynamoDB getAmazonDynamoDB(TestContext testContext) { - return getBeanFactory(testContext).getBean(AmazonDynamoDB.class); - } -} diff --git a/src/test/resources/auditable_user_table.json b/src/test/resources/auditable_user_table.json deleted file mode 100644 index b8406598..00000000 --- a/src/test/resources/auditable_user_table.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "AttributeDefinitions": [ - { - "AttributeName": "id", - "AttributeType": "S" - } - ], - "KeySchema": [ - { - "AttributeName": "id", - "KeyType": "HASH" - } - ], - "ProvisionedThroughput": { - "ReadCapacityUnits": "10", - "WriteCapacityUnits": "10" - }, - "TableName": "auditableUser" -} \ No newline at end of file diff --git a/src/test/resources/customerhistory_table.json b/src/test/resources/customerhistory_table.json deleted file mode 100644 index 7e239f5a..00000000 --- a/src/test/resources/customerhistory_table.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "AttributeDefinitions": [ - { - "AttributeName": "customerId", - "AttributeType": "S" - }, - { - "AttributeName": "createDt", - "AttributeType": "S" - }, - { - "AttributeName": "tag", - "AttributeType": "S" - } - ], - "KeySchema": [ - { - "AttributeName": "customerId", - "KeyType": "HASH" - }, - { - "AttributeName": "createDt", - "KeyType": "RANGE" - } - ], - "ProvisionedThroughput": { - "ReadCapacityUnits": "10", - "WriteCapacityUnits": "10" - }, - "GlobalSecondaryIndexes": [ - { - "IndexName": "idx_global_tag", - "KeySchema": [ - { - "AttributeName": "tag", - "KeyType": "HASH" - } - ], - "Projection": { - "ProjectionType": "ALL" - }, - "ProvisionedThroughput": { - "ReadCapacityUnits": 10, - "WriteCapacityUnits": 10 - } - } - ], - "TableName": "customerhistory" -} diff --git a/src/test/resources/feeduser_table.json b/src/test/resources/feeduser_table.json deleted file mode 100755 index 6fa99932..00000000 --- a/src/test/resources/feeduser_table.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "AttributeDefinitions": [ - { - "AttributeName": "id", - "AttributeType": "S" - }, - { - "AttributeName": "usrNo", - "AttributeType": "N" - }, - { - "AttributeName": "feedOpenYn", - "AttributeType": "N" - } - ], - "KeySchema": [ - { - "AttributeName": "id", - "KeyType": "HASH" - } - ], - "ProvisionedThroughput": { - "ReadCapacityUnits": "10", - "WriteCapacityUnits": "10" - }, - "GlobalSecondaryIndexes": [ - { - "IndexName": "idx_global_usrNo_feedOpenYn", - "KeySchema": [ - { - "AttributeName": "usrNo", - "KeyType": "HASH" - }, - { - "AttributeName": "feedOpenYn", - "KeyType": "RANGE" - } - - ], - "Projection": { - "ProjectionType": "ALL" - }, - "ProvisionedThroughput": { - "ReadCapacityUnits": 10, - "WriteCapacityUnits": 10 - } - } - ], - "TableName": "feed_user" -} diff --git a/src/test/resources/installation_table.json b/src/test/resources/installation_table.json deleted file mode 100644 index 7bb1fb14..00000000 --- a/src/test/resources/installation_table.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "AttributeDefinitions": [ - { - "AttributeName": "id", - "AttributeType": "S" - }, - { - "AttributeName": "systemId", - "AttributeType": "S" - }, - { - "AttributeName": "updatedAt", - "AttributeType": "S" - } - ], - "KeySchema": [ - { - "AttributeName": "id", - "KeyType": "HASH" - } - ], - "ProvisionedThroughput": { - "ReadCapacityUnits": "1", - "WriteCapacityUnits": "1" - }, - "GlobalSecondaryIndexes": [ - { - "IndexName": "idx-global-systemid", - "KeySchema": [ - { - "AttributeName": "systemId", - "KeyType": "HASH" - }, - { - "AttributeName": "updatedAt", - "KeyType": "RANGE" - } - ], - "Projection": { - "ProjectionType": "ALL" - }, - "ProvisionedThroughput": { - "ReadCapacityUnits": 1, - "WriteCapacityUnits": 1 - } - } - ], - "TableName": "installations" -} diff --git a/src/test/resources/playlist_table.json b/src/test/resources/playlist_table.json deleted file mode 100755 index 14ab1d91..00000000 --- a/src/test/resources/playlist_table.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "AttributeDefinitions": [ - { - "AttributeName": "UserName", - "AttributeType": "S" - }, - { - "AttributeName": "PlaylistName", - "AttributeType": "S" - } - ], - "KeySchema": [ - { - "AttributeName": "UserName", - "KeyType": "HASH" - }, - { - "AttributeName": "PlaylistName", - "KeyType": "RANGE" - } - ], - "ProvisionedThroughput": { - "ReadCapacityUnits": "10", - "WriteCapacityUnits": "10" - }, - "TableName": "playlist" -} \ No newline at end of file diff --git a/src/test/resources/user_table.json b/src/test/resources/user_table.json deleted file mode 100755 index efe6b2e5..00000000 --- a/src/test/resources/user_table.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "AttributeDefinitions": [ - { - "AttributeName": "Id", - "AttributeType": "S" - } - ], - "KeySchema": [ - { - "AttributeName": "Id", - "KeyType": "HASH" - } - ], - "ProvisionedThroughput": { - "ReadCapacityUnits": "10", - "WriteCapacityUnits": "10" - }, - "TableName": "user" -} \ No newline at end of file