Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

dynamodb plugin for Amazon AWS DynamoDB: initial version

  • Loading branch information...
commit 394903be0ef4455f1c6e2bd4e64e9350bd39f7e8 1 parent af8d91b
@rstepanenko rstepanenko authored
Showing with 9,724 additions and 0 deletions.
  1. +6 −0 grails-datastore-dynamodb/build.gradle
  2. +43 −0 ...atastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/DelayAfterWriteDynamoDBSession.java
  3. +233 −0 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/DynamoDBDatastore.java
  4. +81 −0 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/DynamoDBSession.java
  5. +96 −0 ...re-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/config/DynamoDBDomainClassMappedForm.java
  6. +64 −0 ...datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/config/DynamoDBMappingContext.java
  7. +48 −0 ...tastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/config/DynamoDBPersistentEntity.java
  8. +58 −0 ...store-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/config/GormDynamoDBMappingFactory.java
  9. +39 −0 ...re-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/AbstractDynamoDBTableResolver.java
  10. +62 −0 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/AssociationKey.java
  11. +42 −0 ...store-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/ConstDynamoDBTableResolver.java
  12. +116 −0 ...store-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBAssociationIndexer.java
  13. +39 −0 ...atastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBAssociationInfo.java
  14. +279 −0 ...atastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBEntityPersister.java
  15. +148 −0 ...atastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBHiLoIdGenerator.java
  16. +12 −0 ...ls-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBIdGenerator.java
  17. +63 −0 ...store-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBIdGeneratorFactory.java
  18. +74 −0 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBNativeItem.java
  19. +48 −0 ...-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBTableResolver.java
  20. +41 −0 ...ore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBTableResolverFactory.java
  21. +15 −0 ...atastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBUUIDIdGenerator.java
  22. +34 −0 ...amodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/model/types/DynamoDBTypeConverterRegistrar.java
  23. +553 −0 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/query/DynamoDBQuery.java
  24. +11 −0 ...astore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/util/DataStoreOperationException.java
  25. +116 −0 ...db/src/main/groovy/org/grails/datastore/mapping/dynamodb/util/DelayAfterWriteDynamoDBTemplateDecorator.java
  26. +55 −0 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/util/DynamoDBConst.java
  27. +45 −0 ...ls-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/util/DynamoDBConverterUtil.java
  28. +158 −0 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/util/DynamoDBTemplate.java
  29. +458 −0 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/util/DynamoDBTemplateImpl.java
  30. +281 −0 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/util/DynamoDBUtil.java
  31. +38 −0 grails-datastore-gorm-dynamodb/build.gradle
  32. +36 −0 grails-datastore-gorm-dynamodb/src/main/groovy/org/grails/datastore/gorm/dynamodb/DynamoDBCriteriaBuilder.java
  33. +126 −0 grails-datastore-gorm-dynamodb/src/main/groovy/org/grails/datastore/gorm/dynamodb/DynamoDBGormEnhancer.groovy
  34. +57 −0 ...ynamodb/src/main/groovy/org/grails/datastore/gorm/dynamodb/bean/factory/DynamoDBDatastoreFactoryBean.groovy
  35. +18 −0 ...db/src/main/groovy/org/grails/datastore/gorm/dynamodb/bean/factory/DynamoDBMappingContextFactoryBean.groovy
  36. +186 −0 ...c/main/groovy/org/grails/datastore/gorm/dynamodb/plugin/support/DynamoDBApplicationContextConfigurer.groovy
  37. +65 −0 ...dynamodb/src/main/groovy/org/grails/datastore/gorm/dynamodb/plugin/support/DynamoDBMethodsConfigurer.groovy
  38. +34 −0 ...m-dynamodb/src/main/groovy/org/grails/datastore/gorm/dynamodb/plugin/support/DynamoDBOnChangeHandler.groovy
  39. +64 −0 ...-dynamodb/src/main/groovy/org/grails/datastore/gorm/dynamodb/plugin/support/DynamoDBSpringConfigurer.groovy
  40. +18 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Book.groovy
  41. +25 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/ChildEntity.groovy
  42. +18 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/City.groovy
  43. +29 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/ClassWithListArgBeforeValidate.groovy
  44. +27 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/ClassWithNoArgBeforeValidate.groovy
  45. +32 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/ClassWithOverloadedBeforeValidate.groovy
  46. +57 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/CommonTypes.groovy
  47. +20 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/ConstrainedEntity.groovy
  48. +29 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Country.groovy
  49. +158 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/CriteriaBuilderSpec.groovy
  50. +244 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/DetachedCriteriaSpec.groovy
  51. +61 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/DynamoDBCombinationSpec.groovy
  52. +18 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/EnumThing.groovy
  53. +11 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Face.groovy
  54. +32 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/GroupWithin.groovy
  55. +19 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Highway.groovy
  56. +75 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/InheritanceSpec.groovy
  57. +371 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/LexComparisonSpec.groovy
  58. +23 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Location.groovy
  59. +22 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/ModifyPerson.groovy
  60. +31 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/MultilineValueSpec.groovy
  61. +67 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/NegationSpec.groovy
  62. +11 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Nose.groovy
  63. +21 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/OptLockNotVersioned.groovy
  64. +17 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/OptLockVersioned.groovy
  65. +57 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/PagedResultSpec.groovy
  66. +31 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Person.groovy
  67. +66 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/PersonEvent.groovy
  68. +36 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Pet.groovy
  69. +20 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/PetType.groovy
  70. +18 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Plant.groovy
  71. +20 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/PlantCategory.groovy
  72. +31 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/PlantNumericIdValue.groovy
  73. +14 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/PropertyComparisonQuerySpec.groovy
  74. +87 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Publication.groovy
  75. +36 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/SimpleDBHiloSpec.groovy
  76. +15 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/SizeQuerySpec.groovy
  77. +26 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/Task.groovy
  78. +42 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/TestEntity.groovy
  79. +29 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/UniqueGroup.groovy
  80. +180 −0 grails-datastore-gorm-dynamodb/src/test/groovy/grails/gorm/tests/ValidationSpec.groovy
  81. +168 −0 grails-datastore-gorm-dynamodb/src/test/groovy/org/grails/datastore/gorm/Setup.groovy
  82. +3 −0  grails-documentation-dynamodb/build.gradle
  83. +4 −0 grails-documentation-dynamodb/src/docs/doc.properties
  84. +6 −0 grails-documentation-dynamodb/src/docs/guide/gettingStarted.gdoc
  85. +83 −0 grails-documentation-dynamodb/src/docs/guide/gettingStarted/configurationOptions.gdoc
  86. +21 −0 grails-documentation-dynamodb/src/docs/guide/introduction.gdoc
  87. +38 −0 grails-documentation-dynamodb/src/docs/guide/introduction/currentFeatureSet.gdoc
  88. +50 −0 grails-documentation-dynamodb/src/docs/guide/introduction/dynamoDBSpecifics.gdoc
  89. +83 −0 grails-documentation-dynamodb/src/docs/guide/mapping.gdoc
  90. +29 −0 grails-documentation-dynamodb/src/docs/guide/mapping/identityGeneration.gdoc
  91. +17 −0 grails-documentation-dynamodb/src/docs/guide/mapping/provisionedThroughput.gdoc
  92. +13 −0 grails-documentation-dynamodb/src/docs/guide/releaseNotes.gdoc
  93. +13 −0 grails-documentation-dynamodb/src/docs/guide/toc.yml
  94. +29 −0 grails-documentation-dynamodb/src/docs/guide/transactions.gdoc
  95. +5 −0 grails-plugins/dynamodb/application.properties
  96. +59 −0 grails-plugins/dynamodb/grails-app/conf/BuildConfig.groovy
  97. +24 −0 grails-plugins/dynamodb/grails-app/conf/Config.groovy
  98. +32 −0 grails-plugins/dynamodb/grails-app/conf/DataSource.groovy
  99. 0  grails-plugins/dynamodb/grails-app/i18n/messages.properties
  100. +10 −0 grails-plugins/dynamodb/scripts/_Install.groovy
  101. +5 −0 grails-plugins/dynamodb/scripts/_Uninstall.groovy
  102. +10 −0 grails-plugins/dynamodb/scripts/_Upgrade.groovy
  103. +33 −0 grails-plugins/dynamodb/web-app/WEB-INF/applicationContext.xml
  104. +14 −0 grails-plugins/dynamodb/web-app/WEB-INF/sitemesh.xml
  105. +572 −0 grails-plugins/dynamodb/web-app/WEB-INF/tld/c.tld
  106. +671 −0 grails-plugins/dynamodb/web-app/WEB-INF/tld/fmt.tld
  107. +550 −0 grails-plugins/dynamodb/web-app/WEB-INF/tld/grails.tld
  108. +311 −0 grails-plugins/dynamodb/web-app/WEB-INF/tld/spring.tld
  109. +109 −0 grails-plugins/dynamodb/web-app/css/errors.css
  110. +585 −0 grails-plugins/dynamodb/web-app/css/main.css
  111. +82 −0 grails-plugins/dynamodb/web-app/css/mobile.css
  112. BIN  grails-plugins/dynamodb/web-app/images/apple-touch-icon-retina.png
  113. BIN  grails-plugins/dynamodb/web-app/images/apple-touch-icon.png
  114. BIN  grails-plugins/dynamodb/web-app/images/favicon.ico
  115. BIN  grails-plugins/dynamodb/web-app/images/grails_logo.jpg
  116. BIN  grails-plugins/dynamodb/web-app/images/grails_logo.png
  117. BIN  grails-plugins/dynamodb/web-app/images/leftnav_btm.png
  118. BIN  grails-plugins/dynamodb/web-app/images/leftnav_midstretch.png
  119. BIN  grails-plugins/dynamodb/web-app/images/leftnav_top.png
  120. BIN  grails-plugins/dynamodb/web-app/images/skin/database_add.png
  121. BIN  grails-plugins/dynamodb/web-app/images/skin/database_delete.png
  122. BIN  grails-plugins/dynamodb/web-app/images/skin/database_edit.png
  123. BIN  grails-plugins/dynamodb/web-app/images/skin/database_save.png
  124. BIN  grails-plugins/dynamodb/web-app/images/skin/database_table.png
  125. BIN  grails-plugins/dynamodb/web-app/images/skin/exclamation.png
  126. BIN  grails-plugins/dynamodb/web-app/images/skin/house.png
  127. BIN  grails-plugins/dynamodb/web-app/images/skin/information.png
  128. BIN  grails-plugins/dynamodb/web-app/images/skin/shadow.jpg
  129. BIN  grails-plugins/dynamodb/web-app/images/skin/sorted_asc.gif
  130. BIN  grails-plugins/dynamodb/web-app/images/skin/sorted_desc.gif
  131. BIN  grails-plugins/dynamodb/web-app/images/spinner.gif
  132. BIN  grails-plugins/dynamodb/web-app/images/springsource.png
  133. +9 −0 grails-plugins/dynamodb/web-app/js/application.js
View
6 grails-datastore-dynamodb/build.gradle
@@ -0,0 +1,6 @@
+version = "0.1.BUILD-SNAPSHOT"
+
+dependencies {
+ compile project(":grails-datastore-core")
+ compile('com.amazonaws:aws-java-sdk:1.3.3')
+}
View
43 ...store-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/DelayAfterWriteDynamoDBSession.java
@@ -0,0 +1,43 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb;
+
+import org.grails.datastore.mapping.cache.TPCacheAdapterRepository;
+import org.grails.datastore.mapping.model.MappingContext;
+import org.springframework.context.ApplicationEventPublisher;
+
+/**
+ * Simple extension used in testing to fight eventual consistency of DynamoDB.
+ */
+public class DelayAfterWriteDynamoDBSession extends DynamoDBSession {
+
+ private long delayMillis;
+
+ public DelayAfterWriteDynamoDBSession(DynamoDBDatastore datastore, MappingContext mappingContext, ApplicationEventPublisher publisher, long delayMillis, TPCacheAdapterRepository cacheAdapterRepository) {
+ super(datastore, mappingContext, publisher, cacheAdapterRepository);
+ this.delayMillis = delayMillis;
+ }
+
+ @Override
+ protected void postFlush(boolean hasUpdates) {
+ if (hasUpdates) {
+ pause();
+ }
+ }
+
+ private void pause(){
+ try { Thread.sleep(delayMillis); } catch (InterruptedException e) { /* ignored */ }
+ }
+}
View
233 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/DynamoDBDatastore.java
@@ -0,0 +1,233 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb;
+
+import org.grails.datastore.mapping.cache.TPCacheAdapterRepository;
+import org.grails.datastore.mapping.core.AbstractDatastore;
+import org.grails.datastore.mapping.core.Session;
+import org.grails.datastore.mapping.dynamodb.engine.*;
+import org.grails.datastore.mapping.model.MappingContext;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.types.Association;
+import org.grails.datastore.mapping.model.types.OneToMany;
+import org.grails.datastore.mapping.dynamodb.config.DynamoDBMappingContext;
+import org.grails.datastore.mapping.dynamodb.model.types.DynamoDBTypeConverterRegistrar;
+import org.grails.datastore.mapping.dynamodb.util.DelayAfterWriteDynamoDBTemplateDecorator;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBTemplate;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBTemplateImpl;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBUtil;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.convert.converter.ConverterRegistry;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.grails.datastore.mapping.config.utils.ConfigUtils.read;
+
+/**
+ * A Datastore implementation for the AWS DynamoDB document store.
+ *
+ * @author Roman Stepanenko based on Graeme Rocher code for MongoDb and Redis
+ * @since 0.1
+ */
+public class DynamoDBDatastore extends AbstractDatastore implements InitializingBean, MappingContext.Listener {
+
+ public static final String SECRET_KEY = "secretKey";
+ public static final String ACCESS_KEY = "accessKey";
+ public static final String TABLE_NAME_PREFIX_KEY = "tableNamePrefix";
+ public static final String DEFAULT_READ_CAPACITY_UNITS = "defaultReadCapacityUnits";
+ public static final String DEFAULT_WRITE_CAPACITY_UNITS = "defaultWriteCapacityUnits";
+ public static final String DELAY_AFTER_WRITES_MS = "delayAfterWritesMS"; //used for testing - to fight eventual consistency if this flag value is 'true' it will add specified pause after writes
+
+ private DynamoDBTemplate dynamoDBTemplate; //currently there is no need to create template per entity, we can share same instance
+ protected Map<AssociationKey, DynamoDBAssociationInfo> associationInfoMap = new HashMap<AssociationKey, DynamoDBAssociationInfo>(); //contains entries only for those associations that need a dedicated table
+ protected Map<PersistentEntity, DynamoDBTableResolver> entityDomainResolverMap = new HashMap<PersistentEntity, DynamoDBTableResolver>();
+ protected Map<PersistentEntity, DynamoDBIdGenerator> entityIdGeneratorMap = new HashMap<PersistentEntity, DynamoDBIdGenerator>();
+
+ private String tableNamePrefix;
+
+ private long defaultReadCapacityUnits;
+ private long defaultWriteCapacityUnits;
+
+ public DynamoDBDatastore() {
+ this(new DynamoDBMappingContext(), Collections.<String, String>emptyMap(), null, null);
+ }
+
+ /**
+ * Constructs a DynamoDBDatastore using the given MappingContext and connection details map.
+ *
+ * @param mappingContext The DynamoDBMappingContext
+ * @param connectionDetails The connection details containing the {@link #ACCESS_KEY} and {@link #SECRET_KEY} settings
+ */
+ public DynamoDBDatastore(MappingContext mappingContext,
+ Map<String, String> connectionDetails, ConfigurableApplicationContext ctx, TPCacheAdapterRepository<DynamoDBNativeItem> adapterRepository) {
+ super(mappingContext, connectionDetails, ctx, adapterRepository);
+
+ if (mappingContext != null) {
+ mappingContext.addMappingContextListener(this);
+ }
+
+ initializeConverters(mappingContext);
+
+ tableNamePrefix = read(String.class, TABLE_NAME_PREFIX_KEY, connectionDetails, null);
+ defaultReadCapacityUnits = read(Long.class, DEFAULT_READ_CAPACITY_UNITS, connectionDetails, (long)3); //minimum for the account in us-east-1 is 3
+ defaultWriteCapacityUnits = read(Long.class, DEFAULT_WRITE_CAPACITY_UNITS, connectionDetails, (long)5); //minimum for the account in us-east-1 is 5
+ }
+
+ public DynamoDBDatastore(MappingContext mappingContext, Map<String, String> connectionDetails) {
+ this(mappingContext, connectionDetails, null, null);
+ }
+
+ public DynamoDBDatastore(MappingContext mappingContext) {
+ this(mappingContext, Collections.<String, String>emptyMap(), null, null);
+ }
+
+ public DynamoDBTemplate getDynamoDBTemplate(@SuppressWarnings("unused") PersistentEntity entity) {
+// return dynamoDBTemplates.get(entity);
+ return dynamoDBTemplate;
+ }
+
+ public DynamoDBTemplate getDynamoDBTemplate() {
+ return dynamoDBTemplate;
+ }
+
+ @Override
+ protected Session createSession(Map<String, String> connDetails) {
+ String delayAfterWrite = read(String.class, DELAY_AFTER_WRITES_MS, connectionDetails, null);
+
+ if (delayAfterWrite != null && !"".equals(delayAfterWrite)) {
+ return new DelayAfterWriteDynamoDBSession(this, getMappingContext(), getApplicationEventPublisher(), Integer.parseInt(delayAfterWrite), cacheAdapterRepository);
+ }
+ return new DynamoDBSession(this, getMappingContext(), getApplicationEventPublisher(), cacheAdapterRepository);
+ }
+
+ public void afterPropertiesSet() throws Exception {
+// for (PersistentEntity entity : mappingContext.getPersistentEntities()) {
+ // Only create DynamoDB templates for entities that are mapped with DynamoDB
+// if (!entity.isExternal()) {
+// createDynamoDBTemplate(entity);
+// }
+// }
+ createDynamoDBTemplate();
+}
+
+ protected void createDynamoDBTemplate() {
+ if (dynamoDBTemplate != null) {
+ return;
+ }
+
+ String accessKey = read(String.class, ACCESS_KEY, connectionDetails, null);
+ String secretKey = read(String.class, SECRET_KEY, connectionDetails, null);
+ String delayAfterWrite = read(String.class, DELAY_AFTER_WRITES_MS, connectionDetails, null);
+
+ dynamoDBTemplate = new DynamoDBTemplateImpl(accessKey, secretKey);
+ if (delayAfterWrite != null && !"".equals(delayAfterWrite)) {
+ dynamoDBTemplate = new DelayAfterWriteDynamoDBTemplateDecorator(dynamoDBTemplate, Integer.parseInt(delayAfterWrite));
+ }
+ }
+
+ /**
+ * If specified, returns table name prefix so that same AWS account can be used for more than one environment (DEV/TEST/PROD etc).
+ * @return null if name was not specified in the configuration
+ */
+ public String getTableNamePrefix() {
+ return tableNamePrefix;
+ }
+
+ public long getDefaultWriteCapacityUnits() {
+ return defaultWriteCapacityUnits;
+ }
+
+ public long getDefaultReadCapacityUnits() {
+ return defaultReadCapacityUnits;
+ }
+
+ public void persistentEntityAdded(PersistentEntity entity) {
+ createDynamoDBTemplate();
+ analyzeAssociations(entity);
+ createEntityDomainResolver(entity);
+ createEntityIdGenerator(entity);
+ }
+
+ /**
+ * If the specified association has a dedicated AWS table, returns info for that association,
+ * otherwise returns null.
+ */
+ public DynamoDBAssociationInfo getAssociationInfo(Association<?> association) {
+ return associationInfoMap.get(generateAssociationKey(association));
+ }
+
+ /**
+ * Returns table resolver for the specified entity.
+ * @param entity
+ * @return
+ */
+ public DynamoDBTableResolver getEntityDomainResolver(PersistentEntity entity) {
+ return entityDomainResolverMap.get(entity);
+ }
+
+ /**
+ * Returns id generator for the specified entity.
+ * @param entity
+ * @return
+ */
+ public DynamoDBIdGenerator getEntityIdGenerator(PersistentEntity entity) {
+ return entityIdGeneratorMap.get(entity);
+ }
+
+ protected void createEntityDomainResolver(PersistentEntity entity) {
+ DynamoDBTableResolverFactory resolverFactory = new DynamoDBTableResolverFactory();
+ DynamoDBTableResolver tableResolver = resolverFactory.buildResolver(entity, this);
+
+ entityDomainResolverMap.put(entity, tableResolver);
+ }
+
+ protected void createEntityIdGenerator(PersistentEntity entity) {
+ DynamoDBIdGeneratorFactory factory = new DynamoDBIdGeneratorFactory();
+ DynamoDBIdGenerator generator = factory.buildIdGenerator(entity, this);
+
+ entityIdGeneratorMap.put(entity, generator);
+ }
+
+ @Override
+ protected void initializeConverters(@SuppressWarnings("hiding") MappingContext mappingContext) {
+ final ConverterRegistry conversionService = mappingContext.getConverterRegistry();
+ new DynamoDBTypeConverterRegistrar().register(conversionService);
+ }
+
+ /**
+ * Analyzes associations and for those associations that need to be stored
+ * in a dedicated AWS table, creates info object with details for that association.
+ */
+ protected void analyzeAssociations(PersistentEntity entity) {
+ for (Association<?> association : entity.getAssociations()) {
+ if (association instanceof OneToMany && !association.isBidirectional()) {
+ String associationDomainName = generateAssociationDomainName(association);
+ associationInfoMap.put(generateAssociationKey(association), new DynamoDBAssociationInfo(associationDomainName));
+ }
+ }
+ }
+
+ protected AssociationKey generateAssociationKey(Association<?> association) {
+ return new AssociationKey(association.getOwner(), association.getName());
+ }
+
+ protected String generateAssociationDomainName(Association<?> association) {
+ String ownerDomainName = DynamoDBUtil.getMappedTableName(association.getOwner());
+ return DynamoDBUtil.getPrefixedTableName(tableNamePrefix, ownerDomainName.toUpperCase() + "_" + association.getName().toUpperCase());
+ }
+}
View
81 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/DynamoDBSession.java
@@ -0,0 +1,81 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb;
+
+import org.grails.datastore.mapping.cache.TPCacheAdapterRepository;
+import org.grails.datastore.mapping.core.AbstractSession;
+import org.grails.datastore.mapping.engine.Persister;
+import org.grails.datastore.mapping.model.MappingContext;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.dynamodb.engine.DynamoDBEntityPersister;
+import org.grails.datastore.mapping.dynamodb.query.DynamoDBQuery;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBTemplate;
+import org.grails.datastore.mapping.transactions.SessionOnlyTransaction;
+import org.grails.datastore.mapping.transactions.Transaction;
+import org.springframework.context.ApplicationEventPublisher;
+
+/**
+ * A {@link org.grails.datastore.mapping.core.Session} implementation
+ * for the AWS DynamoDB store.
+ *
+ * @author Roman Stepanenko based on Graeme Rocher code for MongoDb and Redis
+ * @since 0.1
+ */
+public class DynamoDBSession extends AbstractSession {
+
+ DynamoDBDatastore dynamoDBDatastore;
+
+ public DynamoDBSession(DynamoDBDatastore datastore, MappingContext mappingContext, ApplicationEventPublisher publisher, TPCacheAdapterRepository cacheAdapterRepository) {
+ super(datastore, mappingContext, publisher, cacheAdapterRepository);
+ this.dynamoDBDatastore = datastore;
+ }
+
+ @Override
+ public DynamoDBQuery createQuery(@SuppressWarnings("rawtypes") Class type) {
+ return (DynamoDBQuery) super.createQuery(type);
+ }
+
+/* @Override
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ protected void flushPendingInserts(Map<PersistentEntity, Collection<PendingInsert>> inserts) {
+ //todo - optimize multiple inserts using batch put (make the number of threshold objects configurable)
+ for (final PersistentEntity entity : inserts.keySet()) {
+ final DynamoDBTemplate template = getDynamoDBTemplate(entity.isRoot() ? entity : entity.getRootEntity());
+
+ throw new RuntimeException("not implemented yet");
+// template.persist(null); //todo - :)
+ }
+ }
+ */
+
+ public Object getNativeInterface() {
+ return null; //todo
+ }
+
+ @Override
+ protected Persister createPersister(@SuppressWarnings("rawtypes") Class cls, MappingContext mappingContext) {
+ final PersistentEntity entity = mappingContext.getPersistentEntity(cls.getName());
+ return entity == null ? null : new DynamoDBEntityPersister(mappingContext, entity, this, publisher, cacheAdapterRepository);
+ }
+
+ @Override
+ protected Transaction beginTransactionInternal() {
+ return new SessionOnlyTransaction(null, this);
+ }
+
+ public DynamoDBTemplate getDynamoDBTemplate() {
+ return dynamoDBDatastore.getDynamoDBTemplate();
+ }
+}
View
96 ...dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/config/DynamoDBDomainClassMappedForm.java
@@ -0,0 +1,96 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.config;
+
+import org.grails.datastore.mapping.keyvalue.mapping.config.Family;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBConst;
+
+import java.util.Map;
+
+/**
+ * Mapping for
+ * {@link org.grails.datastore.mapping.dynamodb.config.DynamoDBPersistentEntity}
+ * with the DynamoDB specific properties so that the following can be used in
+ * the mapping:
+ *
+ * <pre>
+ * static mapping = {
+ * table 'Person'
+ * id_generator type:'hilo', maxLo:100 //optional, if not specified UUID is used
+ * throughput read:10, write:5 //optional, if not specified default values will be used
+ * }
+ * </pre>
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+public class DynamoDBDomainClassMappedForm extends Family {
+
+ protected String table;
+ protected Map<String, Object> id_generator; //id generation configuration
+ protected Map<String, Object> throughput; //throughput configuration
+
+ public DynamoDBDomainClassMappedForm() {
+ }
+
+ public DynamoDBDomainClassMappedForm(String table) {
+ this.table = table;
+ }
+
+ public DynamoDBDomainClassMappedForm(String keyspace, String table) {
+ super(keyspace, table);
+ this.table = table;
+ }
+
+ public String getTable() {
+ return table;
+ }
+
+ public void setTable(String table) {
+ this.table = table;
+ super.setFamily(table);
+ }
+
+ @Override
+ public void setFamily(String family) {
+ super.setFamily(family);
+ table = family;
+ }
+
+ public Map<String, Object> getId_generator() {
+ return id_generator;
+ }
+
+ public void setId_generator(Map<String, Object> id_generator) {
+ this.id_generator = id_generator;
+ }
+
+ public Map<String, Object> getThroughput() {
+ return throughput;
+ }
+
+ public void setThroughput(Map<String, Object> throughput) {
+ this.throughput = throughput;
+ }
+
+
+ @Override
+ public String toString() {
+ return "DynamoDBDomainClassMappedForm{" +
+ "table='" + table + '\'' +
+ ", id_generator=" + id_generator +
+ ", throughput=" + throughput +
+ '}';
+ }
+}
View
64 ...astore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/config/DynamoDBMappingContext.java
@@ -0,0 +1,64 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.config;
+
+import org.grails.datastore.mapping.document.config.Attribute;
+import org.grails.datastore.mapping.document.config.Collection;
+import org.grails.datastore.mapping.model.AbstractMappingContext;
+import org.grails.datastore.mapping.model.MappingConfigurationStrategy;
+import org.grails.datastore.mapping.model.MappingFactory;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.config.GormMappingConfigurationStrategy;
+
+/**
+ * Models a {@link org.grails.datastore.mapping.model.MappingContext} for DynamoDB.
+ *
+ * @author Roman Stepanenko based on Graeme Rocher code for MongoDb and Redis
+ * @since 0.1
+ */
+public class DynamoDBMappingContext extends AbstractMappingContext {
+
+ protected MappingConfigurationStrategy syntaxStrategy;
+ MappingFactory<Collection, Attribute> mappingFactory;
+
+ public DynamoDBMappingContext() {
+ mappingFactory = createMappingFactory();
+ syntaxStrategy = new GormMappingConfigurationStrategy(mappingFactory);
+ }
+
+ protected MappingFactory createMappingFactory() {
+ return new GormDynamoDBMappingFactory();
+ }
+
+ @Override
+ protected PersistentEntity createPersistentEntity(@SuppressWarnings("rawtypes") Class javaClass) {
+ DynamoDBPersistentEntity dynamoDBPersistentEntity = new DynamoDBPersistentEntity(javaClass, this);
+
+ //initialize mapping form for DynamoDBPersistentEntity here - otherwise there are some
+ //problems with the initialization sequence when some properties have OneToOne
+ //(FindOrCreateWhereSpec, FindOrSaveWhereSpec, FindOrSaveWhereSpec was failing)
+ mappingFactory.createMappedForm(dynamoDBPersistentEntity);
+
+ return dynamoDBPersistentEntity;
+ }
+
+ public MappingConfigurationStrategy getMappingSyntaxStrategy() {
+ return syntaxStrategy;
+ }
+
+ public MappingFactory getMappingFactory() {
+ return mappingFactory;
+ }
+}
View
48 ...tore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/config/DynamoDBPersistentEntity.java
@@ -0,0 +1,48 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.config;
+
+import org.grails.datastore.mapping.model.*;
+
+/**
+ * Models a DynamoDB-mapped entity.
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+public class DynamoDBPersistentEntity extends AbstractPersistentEntity<DynamoDBDomainClassMappedForm> {
+
+ public DynamoDBPersistentEntity(Class<?> javaClass, MappingContext context) {
+ super(javaClass, context);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ClassMapping<DynamoDBDomainClassMappedForm> getMapping() {
+ return new DynamoDBClassMapping(this, context);
+ }
+
+ public class DynamoDBClassMapping extends AbstractClassMapping<DynamoDBDomainClassMappedForm> {
+
+ public DynamoDBClassMapping(PersistentEntity entity, MappingContext context) {
+ super(entity, context);
+ }
+
+ @Override
+ public DynamoDBDomainClassMappedForm getMappedForm() {
+ return (DynamoDBDomainClassMappedForm) context.getMappingFactory().createMappedForm(DynamoDBPersistentEntity.this);
+ }
+ }
+}
View
58 ...re-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/config/GormDynamoDBMappingFactory.java
@@ -0,0 +1,58 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.config;
+
+import groovy.lang.Closure;
+import org.grails.datastore.mapping.config.groovy.MappingConfigurationBuilder;
+import org.grails.datastore.mapping.keyvalue.mapping.config.Family;
+import org.grails.datastore.mapping.keyvalue.mapping.config.GormKeyValueMappingFactory;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.config.GormProperties;
+import org.grails.datastore.mapping.reflect.ClassPropertyFetcher;
+
+/**
+ * MappingFactory for DynamoDB.
+ *
+ * @author Roman Stepanenko
+ * @since 0.l
+ */
+public class GormDynamoDBMappingFactory extends GormKeyValueMappingFactory {
+
+ public GormDynamoDBMappingFactory() {
+ super(null);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Family createMappedForm(PersistentEntity entity) {
+ ClassPropertyFetcher cpf = ClassPropertyFetcher.forClass(entity.getJavaClass());
+
+ Closure value = cpf.getStaticPropertyValue(GormProperties.MAPPING, Closure.class);
+ if (value == null) {
+ return new DynamoDBDomainClassMappedForm(entity.getName());
+ }
+
+ Family family = new DynamoDBDomainClassMappedForm();
+ MappingConfigurationBuilder builder = new MappingConfigurationBuilder(family, getPropertyMappedFormType());
+
+ builder.evaluate(value);
+ value = cpf.getStaticPropertyValue(GormProperties.CONSTRAINTS, Closure.class);
+ if (value != null) {
+ builder.evaluate(value);
+ }
+ entityToPropertyMap.put(entity, builder.getProperties());
+ return family;
+ }
+}
View
39 ...dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/AbstractDynamoDBTableResolver.java
@@ -0,0 +1,39 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBUtil;
+
+/**
+ * @author Roman Stepanenko
+ */
+public abstract class AbstractDynamoDBTableResolver implements DynamoDBTableResolver {
+
+ protected String entityFamily;
+ protected String tableNamePrefix;
+
+ public AbstractDynamoDBTableResolver(String entityFamily, String tableNamePrefix) {
+ this.tableNamePrefix = tableNamePrefix;
+ this.entityFamily = DynamoDBUtil.getPrefixedTableName(tableNamePrefix, entityFamily);
+ }
+
+ /**
+ * Helper getter for subclasses.
+ * @return entityFamily
+ */
+ protected String getEntityFamily(){
+ return entityFamily;
+ }
+}
View
62 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/AssociationKey.java
@@ -0,0 +1,62 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+import org.grails.datastore.mapping.model.PersistentEntity;
+
+/**
+ * Simple key object for looking up Associations from a map.
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+public class AssociationKey {
+
+ private PersistentEntity owner;
+ private String name;
+
+ public AssociationKey(PersistentEntity owner, String name) {
+ this.owner = owner;
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ AssociationKey that = (AssociationKey) o;
+
+ if (name != null ? !name.equals(that.name) : that.name != null) {
+ return false;
+ }
+ if (owner != null ? !owner.equals(that.owner) : that.owner != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = owner != null ? owner.hashCode() : 0;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ return result;
+ }
+}
View
42 ...re-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/ConstDynamoDBTableResolver.java
@@ -0,0 +1,42 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * An implementation of the table resolver which assumes there is no sharding -
+ * i.e. always the same table name for all the primary keys (for the same type
+ * of {@link org.grails.datastore.mapping.model.PersistentEntity}
+ */
+public class ConstDynamoDBTableResolver extends AbstractDynamoDBTableResolver {
+
+ private List<String> tables;
+
+ public ConstDynamoDBTableResolver(String entityFamily, String tableNamePrefix) {
+ super(entityFamily, tableNamePrefix); //parent contains the logic for figuring out the final entityFamily
+ tables = new LinkedList<String>();
+ tables.add(getEntityFamily()); // without sharding there is just one table
+ }
+
+ public String resolveTable(String id) {
+ return entityFamily; // without sharding it is always the same one per PersistentEntity
+ }
+
+ public List<String> getAllTablesForEntity() {
+ return tables;
+ }
+}
View
116 ...re-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBAssociationIndexer.java
@@ -0,0 +1,116 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+import com.amazonaws.services.dynamodb.model.*;
+import com.amazonaws.services.simpledb.model.Item;
+import com.amazonaws.services.simpledb.model.ReplaceableAttribute;
+import org.grails.datastore.mapping.engine.AssociationIndexer;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.types.Association;
+import org.grails.datastore.mapping.dynamodb.DynamoDBDatastore;
+import org.grails.datastore.mapping.dynamodb.DynamoDBSession;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBUtil;
+
+import java.util.*;
+
+/**
+ * An {@link org.grails.datastore.mapping.engine.AssociationIndexer} implementation for the DynamoDB store.
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+@SuppressWarnings("rawtypes")
+public class DynamoDBAssociationIndexer implements AssociationIndexer {
+ public static final String FOREIGN_KEY_ATTRIBUTE_NAME = "FK";
+
+ private Association association;
+ private DynamoDBSession session;
+
+ public DynamoDBAssociationIndexer(@SuppressWarnings("unused") DynamoDBNativeItem nativeEntry,
+ Association association, DynamoDBSession session) {
+ this.association = association;
+ this.session = session;
+ }
+
+ public PersistentEntity getIndexedEntity() {
+ return association.getAssociatedEntity();
+ }
+
+ public void index(Object primaryKey, List foreignKeys) {
+// System.out.println("INDEX: index for id: "+primaryKey+", keys: "+foreignKeys+". entry: "+nativeEntry+", association: "+association);
+ if (association.isBidirectional()) { //we use additional table only for unidirectional
+ return;
+ }
+
+ DynamoDBAssociationInfo associationInfo = getDatastore().getAssociationInfo(association);
+ //we store them in a multi-value attribute
+ //and key this collection by the primary key of the entity
+
+ Map<String, AttributeValueUpdate> updateItems = new HashMap<String, AttributeValueUpdate>();
+ //collect all foreign keys into string list
+ List<String> fks = new ArrayList<String>();
+ for (Object foreignKey : foreignKeys) {
+ fks.add(foreignKey.toString());
+ }
+ updateItems.put(FOREIGN_KEY_ATTRIBUTE_NAME,
+ new AttributeValueUpdate()
+ .withAction(AttributeAction.PUT)
+ .withValue(new AttributeValue().withSS(fks)));
+
+ session.getDynamoDBTemplate().updateItem(associationInfo.getTableName(), DynamoDBUtil.createIdKey(primaryKey.toString()), updateItems);
+ }
+
+ public List query(Object primaryKey) {
+// System.out.println("INDEX: query for id: "+primaryKey+". entry: "+nativeEntry+", association: "+association);
+ if (!association.isBidirectional()) { //we use additional table only for unidirectional
+ DynamoDBAssociationInfo associationInfo = getDatastore().getAssociationInfo(association);
+ Map<String, AttributeValue> item = session.getDynamoDBTemplate().get(associationInfo.getTableName(), DynamoDBUtil.createIdKey(primaryKey.toString()));
+ if (item == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return DynamoDBUtil.getAttributeValues(item, FOREIGN_KEY_ATTRIBUTE_NAME);
+ }
+ }
+
+ //for bidirectional onToMany association the use the other entity to refer to this guy's PK via FK
+ DynamoDBTableResolver tableResolver = getDatastore().getEntityDomainResolver(association.getAssociatedEntity());
+
+ Map<String, Condition> filter = new HashMap<String, Condition>();
+ DynamoDBUtil.addSimpleComparison(filter,
+ association.getInverseSide().getName(),
+ ComparisonOperator.EQ.toString(),
+ primaryKey.toString(), false);
+
+ List<Map<String, AttributeValue>> items = session.getDynamoDBTemplate().scan(tableResolver.getAllTablesForEntity().get(0), filter, Integer.MAX_VALUE);
+ if (items.isEmpty()) {
+ return Collections.EMPTY_LIST;
+ }
+ return DynamoDBUtil.collectIds(items);
+ }
+
+ private DynamoDBDatastore getDatastore() {
+ return ((DynamoDBDatastore) session.getDatastore());
+ }
+
+ public void index(Object primaryKey, Object foreignKey) {
+// System.out.println("INDEX: index for id: "+primaryKey+", KEY: "+foreignKey+". entry: "+nativeEntry+", association: "+association);
+ if (association.isBidirectional()) { //we use additional table only for unidirectional
+ return;
+ }
+
+ throw new RuntimeException("not implemented: index(Object primaryKey, Object foreignKey)");
+ }
+}
View
39 ...store-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBAssociationInfo.java
@@ -0,0 +1,39 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+/**
+ * For associations that are stored in dedicated tables (unidirectional onToMany), contains
+ * table name for each association.
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+public class DynamoDBAssociationInfo {
+
+ private String tableName;
+
+ public DynamoDBAssociationInfo(String tableName) {
+ this.tableName = tableName;
+ }
+
+ public String getTableName() {
+ return tableName;
+ }
+
+ public void setTableName(String tableName) {
+ this.tableName = tableName;
+ }
+}
View
279 ...store-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBEntityPersister.java
@@ -0,0 +1,279 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+import com.amazonaws.services.dynamodb.model.AttributeAction;
+import com.amazonaws.services.dynamodb.model.AttributeValue;
+import com.amazonaws.services.dynamodb.model.AttributeValueUpdate;
+import org.grails.datastore.mapping.cache.TPCacheAdapterRepository;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBUtil;
+import org.grails.datastore.mapping.engine.AssociationIndexer;
+import org.grails.datastore.mapping.engine.EntityAccess;
+import org.grails.datastore.mapping.engine.NativeEntryEntityPersister;
+import org.grails.datastore.mapping.engine.PropertyValueIndexer;
+import org.grails.datastore.mapping.model.MappingContext;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.PersistentProperty;
+import org.grails.datastore.mapping.model.types.Association;
+import org.grails.datastore.mapping.query.Query;
+import org.grails.datastore.mapping.dynamodb.DynamoDBDatastore;
+import org.grails.datastore.mapping.dynamodb.DynamoDBSession;
+import org.grails.datastore.mapping.dynamodb.query.DynamoDBQuery;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBConverterUtil;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBTemplate;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.dao.DataAccessException;
+
+import java.io.Serializable;
+import java.util.*;
+
+/**
+ * A {@link org.grails.datastore.mapping.engine.EntityPersister} implementation for the DynamoDB store.
+ *
+ * @author Roman Stepanenko based on Graeme Rocher code for MongoDb and Redis
+ * @since 0.1
+ */
+public class DynamoDBEntityPersister extends NativeEntryEntityPersister<DynamoDBNativeItem, Object> {
+
+ protected DynamoDBTemplate dynamoDBTemplate;
+ protected String entityFamily;
+ protected DynamoDBTableResolver tableResolver;
+ protected DynamoDBIdGenerator idGenerator;
+ protected boolean hasNumericalIdentifier = false;
+ protected boolean hasStringIdentifier = false;
+
+ public DynamoDBEntityPersister(MappingContext mappingContext, PersistentEntity entity,
+ DynamoDBSession dynamoDBSession, ApplicationEventPublisher publisher, TPCacheAdapterRepository<DynamoDBNativeItem> cacheAdapterRepository) {
+ super(mappingContext, entity, dynamoDBSession, publisher, cacheAdapterRepository);
+ DynamoDBDatastore datastore = (DynamoDBDatastore) dynamoDBSession.getDatastore();
+ dynamoDBTemplate = datastore.getDynamoDBTemplate(entity);
+
+ hasNumericalIdentifier = Long.class.isAssignableFrom(entity.getIdentity().getType());
+ hasStringIdentifier = String.class.isAssignableFrom(entity.getIdentity().getType());
+ tableResolver = datastore.getEntityDomainResolver(entity);
+ idGenerator = datastore.getEntityIdGenerator(entity);
+ }
+
+ public Query createQuery() {
+ return new DynamoDBQuery(getSession(), getPersistentEntity(), tableResolver, this, dynamoDBTemplate);
+ }
+
+ public DynamoDBTableResolver getTableResolver() {
+ return tableResolver;
+ }
+
+ @Override
+ protected boolean doesRequirePropertyIndexing() {
+ return false;
+ }
+
+ @Override
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ protected List<Object> retrieveAllEntities(PersistentEntity persistentEntity,
+ Iterable<Serializable> keys) {
+
+ Query query = session.createQuery(persistentEntity.getJavaClass());
+
+ if (keys instanceof List) {
+ if (((List)keys).isEmpty()) {
+ return Collections.EMPTY_LIST;
+ }
+ query.in(persistentEntity.getIdentity().getName(), (List)keys);
+ }
+ else {
+ List<Serializable> keyList = new ArrayList<Serializable>();
+ for (Serializable key : keys) {
+ keyList.add(key);
+ }
+ if (keyList.isEmpty()) {
+ return Collections.EMPTY_LIST;
+ }
+ query.in(persistentEntity.getIdentity().getName(), keyList);
+ }
+
+ List<Object> entityResults = new ArrayList<Object>();
+ Iterator<Serializable> keyIterator = keys.iterator();
+ Iterator<Object> listIterator = query.list().iterator();
+ while (keyIterator.hasNext() && listIterator.hasNext()) {
+ Serializable key = keyIterator.next();
+ Object next = listIterator.next();
+ if (next instanceof DynamoDBNativeItem) {
+ entityResults.add(createObjectFromNativeEntry(getPersistentEntity(), key, (DynamoDBNativeItem)next));
+ }
+ else {
+ entityResults.add(next);
+ }
+ }
+
+ return entityResults;
+ }
+
+ @Override
+ protected List<Object> retrieveAllEntities(PersistentEntity persistentEntity, Serializable[] keys) {
+ return retrieveAllEntities(persistentEntity, Arrays.asList(keys));
+ }
+
+ @Override
+ public String getEntityFamily() {
+ return entityFamily;
+ }
+
+ @Override
+ protected void deleteEntry(String family, Object key, Object entry) {
+ String domain = tableResolver.resolveTable((String) key);
+ dynamoDBTemplate.deleteItem(domain, DynamoDBUtil.createIdKey((String) key));
+ }
+
+ @Override
+ protected Object generateIdentifier(final PersistentEntity persistentEntity,
+ final DynamoDBNativeItem nativeEntry) {
+ return idGenerator.generateIdentifier(persistentEntity, nativeEntry);
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public PropertyValueIndexer getPropertyIndexer(PersistentProperty property) {
+ // We don't need to implement this for DynamoDB since DynamoDB automatically creates indexes for us
+ return null;
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public AssociationIndexer getAssociationIndexer(DynamoDBNativeItem nativeEntry, Association association) {
+ return new DynamoDBAssociationIndexer(nativeEntry, association, (DynamoDBSession) session);
+ }
+
+ @Override
+ protected DynamoDBNativeItem createNewEntry(String family) {
+ return new DynamoDBNativeItem();
+ }
+
+ @Override
+ protected Object getEntryValue(DynamoDBNativeItem nativeEntry, String property) {
+ return nativeEntry.get(property);
+ }
+
+ @Override
+ protected void setEntryValue(DynamoDBNativeItem nativeEntry, String key, Object value) {
+ if (value != null && !getMappingContext().isPersistentEntity(value)) {
+ String stringValue = DynamoDBConverterUtil.convertToString(value, getMappingContext());
+ boolean isNumber = DynamoDBConverterUtil.isNumber(value);
+
+ nativeEntry.put(key, stringValue, isNumber);
+ }
+ }
+
+// @Override
+// protected EntityAccess createEntityAccess(PersistentEntity persistentEntity, Object obj, DynamoDBNativeItem nativeEntry) {
+// final NativeEntryModifyingEntityAccess ea = new DynamoDBNativeEntryModifyingEntityAccess(persistentEntity, obj);
+// ea.setNativeEntry(nativeEntry);
+// return ea;
+// }
+
+ @Override
+ protected DynamoDBNativeItem retrieveEntry(final PersistentEntity persistentEntity,
+ String family, final Serializable key) {
+ String table = tableResolver.resolveTable((String) key);
+ Map<String,AttributeValue> item = dynamoDBTemplate.get(table, DynamoDBUtil.createIdKey((String) key));
+ return item == null ? null : new DynamoDBNativeItem(item);
+ }
+
+ @Override
+ protected Object storeEntry(final PersistentEntity persistentEntity, final EntityAccess entityAccess,
+ final Object storeId, final DynamoDBNativeItem entry) {
+ String id = storeId.toString();
+ String table = tableResolver.resolveTable(id);
+
+ Map<String, AttributeValue> allAttributes = entry.createItem();
+ entry.put("id", id, false);
+
+ dynamoDBTemplate.putItem(table, allAttributes);
+ return storeId; //todo should we return string id here?
+ }
+
+ @Override
+ public void updateEntry(final PersistentEntity persistentEntity, final EntityAccess ea,
+ final Object key, final DynamoDBNativeItem entry) {
+
+ String id = key.toString();
+ String table = tableResolver.resolveTable(id);
+
+ Map<String, AttributeValue> allAttributes = entry.createItem();
+
+ Map<String, AttributeValueUpdate> updates = new HashMap<String, AttributeValueUpdate>();
+
+ //we have to put *new* (incremented) version as part of the 'version' value and use the old version value in the conditional update.
+ //if the update fails we have to restore the version to the old value
+ Object currentVersion = null;
+ String stringCurrentVersion = null;
+ if (isVersioned(ea)) {
+ currentVersion = ea.getProperty("version");
+ stringCurrentVersion = convertVersionToString(currentVersion);
+ incrementVersion(ea); //increment version now before we save it
+ }
+
+ for (Map.Entry<String, AttributeValue> e : allAttributes.entrySet()) {
+ if ("version".equals(e.getKey())) {
+ //ignore it, it will be explicitly added later right before the insert by taking incrementing and taking new one
+ } else if ("id".equals(e.getKey())) {
+ //ignore it, we do not want to mark it as PUT - dynamo will freak out because it is primary key (can't be updated)
+ } else {
+ AttributeValue av = e.getValue();
+ if (av.getS() != null || av.getN() != null) {
+ updates.put(e.getKey(), new AttributeValueUpdate(av, AttributeAction.PUT));
+ } else {
+ updates.put(e.getKey(), new AttributeValueUpdate(null, AttributeAction.DELETE)); //http://stackoverflow.com/questions/9142074/deleting-attribute-in-dynamodb
+ }
+ }
+ }
+
+ if (isVersioned(ea)) {
+ putAttributeForVersion(updates, ea); //update the version
+ try {
+ dynamoDBTemplate.updateItemVersioned(table, DynamoDBUtil.createIdKey(id), updates, stringCurrentVersion, persistentEntity);
+ } catch (DataAccessException e) {
+ //we need to restore version to what it was before the attempt to update
+ ea.setProperty("version", currentVersion);
+ throw e;
+ }
+ } else {
+ dynamoDBTemplate.updateItem(table, DynamoDBUtil.createIdKey(id), updates);
+ }
+ }
+
+ protected void putAttributeForVersion(Map<String, AttributeValueUpdate> updates, EntityAccess ea) {
+ AttributeValueUpdate attrToPut;
+ Object updatedVersion = ea.getProperty("version");
+ String stringUpdatedVersion = convertVersionToString(updatedVersion);
+ attrToPut = new AttributeValueUpdate(new AttributeValue().withN(stringUpdatedVersion),
+ AttributeAction.PUT);
+ updates.put("version", attrToPut);
+ }
+
+ protected String convertVersionToString(Object currentVersion) {
+ if (currentVersion == null) {
+ return null;
+ }
+
+ return currentVersion.toString();
+ }
+
+ @Override
+ protected void deleteEntries(String family, final List<Object> keys) {
+ for (Object key : keys) {
+ deleteEntry(family, key, null); //todo - optimize for bulk removal
+ }
+ }
+}
View
148 ...store-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBHiLoIdGenerator.java
@@ -0,0 +1,148 @@
+package org.grails.datastore.mapping.dynamodb.engine;
+
+import com.amazonaws.AmazonServiceException;
+import com.amazonaws.services.dynamodb.model.AttributeValue;
+import org.grails.datastore.mapping.core.OptimisticLockingException;
+import org.grails.datastore.mapping.dynamodb.DynamoDBDatastore;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBConst;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBUtil;
+import org.springframework.dao.DataAccessException;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Implementation of HiLo generator for DynamoDB.
+ * All HiLows are stored in a single dedicated AWS table. Id of each record is the corresponding table name of the
+ * {@link org.grails.datastore.mapping.model.PersistentEntity}. The only attributes are the nextHi long attribute and the version.
+ *
+ * @author Roman Stepanenko
+ */
+public class DynamoDBHiLoIdGenerator implements DynamoDBIdGenerator {
+ /**
+ * @param table table where the all the counters are stored
+ * @param id name of the domain for some {@link org.grails.datastore.mapping.model.PersistentEntity} for which this instance will be keeping the counter
+ * @param datastore
+ */
+ public DynamoDBHiLoIdGenerator(String table, String id, int lowSize, DynamoDBDatastore datastore) {
+ this.table = table;
+ this.id = id;
+ this.lowSize = lowSize;
+ this.datastore = datastore;
+ }
+
+ public synchronized Object generateIdentifier(PersistentEntity persistentEntity, DynamoDBNativeItem nativeEntry) {
+ if (!initialized) {
+ initialize(persistentEntity);
+ }
+ if (current == max) {
+ incrementDBAndRefresh(persistentEntity);
+ reset();
+ }
+
+ long result = current;
+ current = current + 1;
+ return result;
+ }
+
+ private void reset() {
+ current = currentHi * lowSize;
+ max = current + lowSize;
+ }
+
+ private void incrementDBAndRefresh(PersistentEntity persistentEntity) {
+ boolean done = false;
+ int attempt = 0;
+ while (!done) {
+ attempt++;
+ if (attempt > 10000) {//todo - make configurable at some point
+ throw new IllegalArgumentException("exceeded number of attempts to load new Hi value value from db");
+ }
+ try {
+ Map<String,AttributeValue> item = datastore.getDynamoDBTemplate().getConsistent(table, DynamoDBUtil.createIdKey(id));
+
+ if (item == null) {//no record exist yet
+ currentHi = 1;
+ currentVersion = null;
+ } else {
+ currentHi = Long.parseLong(DynamoDBUtil.getAttributeValueNumeric(item, DynamoDBConst.ID_GENERATOR_HI_LO_ATTRIBUTE_NAME));
+ currentVersion = Long.parseLong(DynamoDBUtil.getAttributeValueNumeric(item, "version"));
+ }
+
+ long nextHi = currentHi + 1;
+ long nextVersion = currentVersion == null ? (long)1: currentVersion+1;
+
+ createOrUpdate(nextHi, nextVersion, currentVersion, persistentEntity);
+ currentVersion = nextVersion;
+
+ done = true;
+ } catch (OptimisticLockingException e) {
+ //collition, it is expected to happen, we will try again
+ }
+ }
+ }
+
+ /**
+ * Create table if needed.
+ */
+ private void initialize(PersistentEntity persistentEntity) {
+ try {
+ Map<String,AttributeValue> item = datastore.getDynamoDBTemplate().getConsistent(table, DynamoDBUtil.createIdKey(id));
+ } catch (DataAccessException e) {
+ throw new RuntimeException(e);
+ } catch (Exception e) {
+ //check if domain does not exist at all
+ AmazonServiceException awsE = null;
+ if (e instanceof AmazonServiceException) {
+ awsE = (AmazonServiceException) e;
+ } else if (e.getCause() instanceof AmazonServiceException) {
+ awsE = (AmazonServiceException) e.getCause();
+ }
+ if (awsE != null && DynamoDBUtil.AWS_ERR_CODE_RESOURCE_NOT_FOUND.equals(awsE.getErrorCode())) {
+ //table does not exist, must create it
+ createHiLoTable(datastore, table);
+ } else {
+ throw new RuntimeException(e);
+ }
+ }
+
+ current = 0;
+ max = 0;
+
+ initialized = true;
+ }
+
+ public static void createHiLoTable(DynamoDBDatastore datastore, String tableName) {
+ datastore.getDynamoDBTemplate().createTable(
+ tableName,
+ DynamoDBUtil.createIdKeySchema(),
+ DynamoDBUtil.createDefaultProvisionedThroughput(datastore));
+ }
+
+ private void createOrUpdate(long nextHi, long newVersion, Long expectedVersion, PersistentEntity persistentEntity) {
+ Map<String, AttributeValue> item = new HashMap<String, AttributeValue>();
+ item.put(DynamoDBConst.ID_GENERATOR_HI_LO_ATTRIBUTE_NAME, new AttributeValue().withN(String.valueOf(nextHi)));
+ item.put("version", new AttributeValue().withN(String.valueOf(newVersion)));
+ DynamoDBUtil.addId(item, id);
+ if (expectedVersion == null) {
+ //since there is no record yet we can't assert on version
+ datastore.getDynamoDBTemplate().putItem(table, item);
+ } else {
+ datastore.getDynamoDBTemplate().putItemVersioned(table, DynamoDBUtil.createIdKey(id), item, String.valueOf(expectedVersion), persistentEntity);
+ }
+ }
+
+
+ private String id;
+ private long current;
+ private int lowSize;
+ private long max;
+
+ private boolean initialized;
+ private long currentHi;
+ private Long currentVersion;
+
+ private DynamoDBDatastore datastore;
+ private String table;
+}
View
12 ...datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBIdGenerator.java
@@ -0,0 +1,12 @@
+package org.grails.datastore.mapping.dynamodb.engine;
+
+import org.grails.datastore.mapping.model.PersistentEntity;
+
+/**
+ * Encapsulates logic for generating id for a DynamoDB object.
+ *
+ * @author Roman Stepanenko
+ */
+public interface DynamoDBIdGenerator {
+ Object generateIdentifier(final PersistentEntity persistentEntity, final DynamoDBNativeItem nativeEntry);
+}
View
63 ...re-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBIdGeneratorFactory.java
@@ -0,0 +1,63 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+import org.grails.datastore.mapping.model.ClassMapping;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.dynamodb.DynamoDBDatastore;
+import org.grails.datastore.mapping.dynamodb.config.DynamoDBDomainClassMappedForm;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBConst;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBUtil;
+
+import java.util.Map;
+
+/**
+ * Encapsulates logic of building appropriately configured DynamoDBIdGenerator instance.
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+public class DynamoDBIdGeneratorFactory {
+
+ public DynamoDBIdGenerator buildIdGenerator(PersistentEntity entity, DynamoDBDatastore dynamoDBDatastore) {
+ String entityFamily = DynamoDBUtil.getMappedTableName(entity);
+
+ @SuppressWarnings("unchecked")
+ ClassMapping<DynamoDBDomainClassMappedForm> classMapping = entity.getMapping();
+ DynamoDBDomainClassMappedForm mappedForm = classMapping.getMappedForm();
+
+ Map<String, Object> generatorInfo = mappedForm.getId_generator();
+
+ //by default use uuid generator
+ if (generatorInfo == null || generatorInfo.isEmpty()) {
+ return new DynamoDBUUIDIdGenerator();
+ }
+
+ String generatorType = (String) generatorInfo.get(DynamoDBConst.PROP_ID_GENERATOR_TYPE);
+ if (DynamoDBConst.PROP_ID_GENERATOR_TYPE_UUID.equals(generatorType)) {
+ return new DynamoDBUUIDIdGenerator();
+ } else if ((DynamoDBConst.PROP_ID_GENERATOR_TYPE_HILO.equals(generatorType))) {
+ Integer lowSize = (Integer) generatorInfo.get(DynamoDBConst.PROP_ID_GENERATOR_MAX_LO);
+ if (lowSize == null) {
+ lowSize = DynamoDBConst.PROP_ID_GENERATOR_MAX_LO_DEFAULT_VALUE; // default value
+ }
+ String hiloDomainName = DynamoDBUtil.getPrefixedTableName(dynamoDBDatastore.getTableNamePrefix(), DynamoDBConst.ID_GENERATOR_HI_LO_TABLE_NAME);
+ return new DynamoDBHiLoIdGenerator(hiloDomainName, entityFamily, lowSize, dynamoDBDatastore);
+ } else {
+ throw new IllegalArgumentException("unknown id generator type for dynamodb: " + generatorType + ". Current implementation supports only " +
+ DynamoDBConst.PROP_ID_GENERATOR_TYPE_UUID + " and " + DynamoDBConst.PROP_ID_GENERATOR_TYPE_HILO);
+ }
+ }
+}
View
74 ...-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBNativeItem.java
@@ -0,0 +1,74 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+import com.amazonaws.services.dynamodb.model.AttributeValue;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBUtil;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Logical representation of how information is loaded from and sent to AWS.
+ * <p/>
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+public class DynamoDBNativeItem {
+
+ private Map<String, AttributeValue> data = new HashMap<String, AttributeValue>(); //todo - not sure about concurrency requirements?
+
+ public DynamoDBNativeItem() {
+ }
+
+ public DynamoDBNativeItem(Map<String, AttributeValue> item) {
+ //populate map with the item attributes. //todo - handle multi-value attributes/long string etc
+ for (Map.Entry<String, AttributeValue> entry : item.entrySet()) {
+ data.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ public void put(String key, String stringValue, boolean isNumber) {
+ data.put(key, DynamoDBUtil.createAttributeValue(stringValue, isNumber));
+ }
+
+ public String get(String key) {
+ AttributeValue attributeValue = data.get(key);
+ if (attributeValue == null) {
+ return null;
+ }
+
+ //it can be either numeric or string format, try first string and then numeric - unfortunately we do not have access to real type
+ String result = attributeValue.getS();
+ if (result == null) {
+ result = attributeValue.getN();
+ }
+
+ return result;
+ }
+
+ public Map<String, AttributeValue> createItem() {
+// Map<String, AttributeValue> result = new HashMap<String, AttributeValue>();
+// result.putAll(data);
+// return result;
+ return data; //this method is used only in read-only fashion, so it is safe to return the inner map
+ }
+
+ @Override
+ public String toString() {
+ return "DynamoDBNativeItem{data=" + data + '}';
+ }
+}
View
48 ...tastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBTableResolver.java
@@ -0,0 +1,48 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+import java.util.List;
+
+/**
+ * Encapsulates logic of determining DynamoDB domain name based specific a
+ * primary key, assuming that this instance of the resolver is used only for one
+ * {@link org.grails.datastore.mapping.model.PersistentEntity}, which
+ * was provided during construction time of this instance.
+ *
+ * Strictly speaking sharding is not really needed for DynamoDB because it is supposed
+ * to provide infinite scalability of a single table, but is left just in case.
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+public interface DynamoDBTableResolver {
+
+ /**
+ * Returns domain name for the specified primary key value.
+ *
+ * @param id
+ * @return
+ */
+ String resolveTable(String id);
+
+ /**
+ * Returns all domain names for this type of entity. Without sharding this
+ * list contains always one element.
+ *
+ * @return
+ */
+ List<String> getAllTablesForEntity();
+}
View
41 ...-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBTableResolverFactory.java
@@ -0,0 +1,41 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.engine;
+
+import org.grails.datastore.mapping.model.ClassMapping;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.dynamodb.DynamoDBDatastore;
+import org.grails.datastore.mapping.dynamodb.config.DynamoDBDomainClassMappedForm;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBUtil;
+
+/**
+ * Encapsulates logic of building appropriately configured DynamoDBTableResolver instance.
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+public class DynamoDBTableResolverFactory {
+
+ public DynamoDBTableResolver buildResolver(PersistentEntity entity, DynamoDBDatastore dynamoDBDatastore) {
+ String entityFamily = DynamoDBUtil.getMappedTableName(entity);
+
+ @SuppressWarnings("unchecked")
+ ClassMapping<DynamoDBDomainClassMappedForm> classMapping = entity.getMapping();
+ DynamoDBDomainClassMappedForm mappedForm = classMapping.getMappedForm();
+
+ return new ConstDynamoDBTableResolver(entityFamily, dynamoDBDatastore.getTableNamePrefix());
+ }
+
+}
View
15 ...store-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/engine/DynamoDBUUIDIdGenerator.java
@@ -0,0 +1,15 @@
+package org.grails.datastore.mapping.dynamodb.engine;
+
+import org.grails.datastore.mapping.model.PersistentEntity;
+
+import java.util.UUID;
+
+/**
+ * Uses java UUID to generate a unique id.
+ * @author Roman Stepanenko
+ */
+public class DynamoDBUUIDIdGenerator implements DynamoDBIdGenerator {
+ public Object generateIdentifier(PersistentEntity persistentEntity, DynamoDBNativeItem nativeEntry) {
+ return UUID.randomUUID().toString();
+ }
+}
View
34 ...db/src/main/groovy/org/grails/datastore/mapping/dynamodb/model/types/DynamoDBTypeConverterRegistrar.java
@@ -0,0 +1,34 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.model.types;
+
+import org.grails.datastore.mapping.model.types.BasicTypeConverterRegistrar;
+import org.springframework.core.GenericTypeResolver;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.convert.converter.ConverterRegistry;
+import org.springframework.core.convert.converter.GenericConverter;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.util.Date;
+
+/**
+ * A registrar that registers type converters used for DynamoDB.
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+public class DynamoDBTypeConverterRegistrar extends BasicTypeConverterRegistrar {
+}
View
553 grails-datastore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/query/DynamoDBQuery.java
@@ -0,0 +1,553 @@
+/* Copyright (C) 2011 SpringSource
+ *
+ * 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.grails.datastore.mapping.dynamodb.query;
+
+import com.amazonaws.services.dynamodb.model.AttributeValue;
+import com.amazonaws.services.dynamodb.model.ComparisonOperator;
+import com.amazonaws.services.dynamodb.model.Condition;
+import org.grails.datastore.mapping.core.Session;
+import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValue;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.model.PersistentProperty;
+import org.grails.datastore.mapping.query.Query;
+import org.grails.datastore.mapping.dynamodb.engine.DynamoDBTableResolver;
+import org.grails.datastore.mapping.dynamodb.engine.DynamoDBEntityPersister;
+import org.grails.datastore.mapping.dynamodb.engine.DynamoDBNativeItem;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBConverterUtil;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBTemplate;
+import org.grails.datastore.mapping.dynamodb.util.DynamoDBUtil;
+import org.grails.datastore.mapping.query.order.ManualEntityOrdering;
+
+import java.util.*;
+
+/**
+ * A {@link org.grails.datastore.mapping.query.Query} implementation for the DynamoDB store
+ *
+ * @author Roman Stepanenko
+ * @since 0.1
+ */
+@SuppressWarnings("rawtypes")
+public class DynamoDBQuery extends Query {
+
+ protected DynamoDBTableResolver tableResolver;
+ protected DynamoDBTemplate dynamoDBTemplate;
+ protected DynamoDBEntityPersister dynamoDBEntityPersister;
+
+ protected static Map<Class, QueryHandler> queryHandlers = new HashMap<Class, QueryHandler>();
+
+ static {
+ //see http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/API_Scan.html
+ //for a list of all dynamodb operators and their arguments
+
+ queryHandlers.put(Equals.class, new QueryHandler<Equals>() {
+ public void handle(PersistentEntity entity, Equals criterion, Map<String, Condition> filter) {
+ String propertyName = criterion.getProperty();
+ String key = getKey(entity, propertyName);
+ if (criterion.getValue() == null) {
+ //for null we have to use special operator
+ DynamoDBUtil.checkFilterForExistingKey(filter, key);
+ filter.put(key, new Condition().withComparisonOperator(ComparisonOperator.NULL.toString()));
+ } else {
+ String stringValue = DynamoDBConverterUtil.convertToString(criterion.getValue(), entity.getMappingContext());
+ boolean isNumber = DynamoDBConverterUtil.isNumber(criterion.getValue());
+
+ DynamoDBUtil.addSimpleComparison(filter, key, ComparisonOperator.EQ.toString(), stringValue, isNumber);
+ }
+
+ }
+ });
+ queryHandlers.put(NotEquals.class, new QueryHandler<NotEquals>() {
+ public void handle(PersistentEntity entity, NotEquals criterion, Map<String, Condition> filter) {
+ String propertyName = criterion.getProperty();
+ String key = getKey(entity, propertyName);
+ if (criterion.getValue() == null) {
+ //for null we have to use special operator
+ DynamoDBUtil.checkFilterForExistingKey(filter, key);
+ filter.put(key, new Condition().withComparisonOperator(ComparisonOperator.NOT_NULL.toString()));
+ } else {
+ String stringValue = DynamoDBConverterUtil.convertToString(criterion.getValue(), entity.getMappingContext());
+ boolean isNumber = DynamoDBConverterUtil.isNumber(criterion.getValue());
+
+ DynamoDBUtil.addSimpleComparison(filter, key, ComparisonOperator.NE.toString(), stringValue, isNumber);
+ }
+ }
+ });
+ queryHandlers.put(IdEquals.class, new QueryHandler<IdEquals>() {
+ public void handle(PersistentEntity entity, IdEquals criterion, Map<String, Condition> filter) {
+ String stringValue = DynamoDBConverterUtil.convertToString(criterion.getValue(), entity.getMappingContext());
+
+ DynamoDBUtil.addSimpleComparison(filter, "id", ComparisonOperator.EQ.toString(), stringValue, false);
+ }
+ });
+ queryHandlers.put(Like.class, new QueryHandler<Like>() {
+ public void handle(PersistentEntity entity, Like criterion, Map<String, Condition> filter) {
+ String propertyName = criterion.getProperty();
+ String key = getKey(entity, propertyName);
+ String stringValue = DynamoDBConverterUtil.convertToString(criterion.getValue(), entity.getMappingContext());
+ boolean isNumber = DynamoDBConverterUtil.isNumber(criterion.getValue());
+
+
+ //dynamo db has only 'contains' and 'begins_with' operators, so we have to take out '%' and figure out which one to use
+
+
+ String searchToken = stringValue;
+
+ //begins_with is without % at all or (xxx% and not %xxx%)
+ if (!searchToken.contains("%") || (searchToken.endsWith("%") && !searchToken.startsWith("%"))) {
+ if (searchToken.endsWith("%")) {
+ //kill % at the end
+ searchToken = searchToken.substring(0, searchToken.length() - 1);
+ }
+
+ //make sure % is not in the middle - we can't handle it with dynamo
+ if (searchToken.contains("%")) {
+ throw new IllegalArgumentException("DynamoDB can not handle % in the middle of search string. You specified: " + stringValue);
+ }
+
+ DynamoDBUtil.addSimpleComparison(filter, key, ComparisonOperator.BEGINS_WITH.toString(), searchToken, isNumber);
+ } else {
+ //if we got here it has to start with %
+ //the only supported cases are %xxx and %xxx%
+
+ if (searchToken.endsWith("%")) {
+ //kill % at the end
+ searchToken = searchToken.substring(0, searchToken.length() - 1);
+ }
+
+ if (searchToken.startsWith("%")) {
+ //kill % at the beginning
+ searchToken = searchToken.substring(1, searchToken.length());
+ }
+
+ //make sure % is not in the middle - we can't handle it with dynamo
+ if (searchToken.contains("%")) {
+ throw new IllegalArgumentException("DynamoDB can not handle % in the middle of search string. You specified: " + stringValue);
+ }
+
+ DynamoDBUtil.addSimpleComparison(filter, key, ComparisonOperator.CONTAINS.toString(), searchToken, isNumber);
+ }
+ }
+ });
+ queryHandlers.put(In.class, new QueryHandler<In>() {
+ public void handle(PersistentEntity entity, In criterion, Map<String, Condition> filter) {
+ String propertyName = criterion.getProperty();
+ String key = getKey(entity, propertyName);
+
+ DynamoDBUtil.checkFilterForExistingKey(filter, key);
+
+ Collection<String> stringValues = DynamoDBConverterUtil.convertToStrings(criterion.getValues(), entity.getMappingContext());
+ boolean isNumber = false;
+ if (!criterion.getValues().isEmpty()) {
+ //all values should be of the same type, so take a look at the first
+ isNumber = DynamoDBConverterUtil.isNumber(criterion.getValues().iterator().next());
+ }
+
+ Collection<AttributeValue> attributeValues = new ArrayList<AttributeValue>();
+ for (String stringValue : stringValues) {
+ DynamoDBUtil.addAttributeValue(attributeValues, stringValue, isNumber);
+ }
+
+ filter.put(key, new Condition().withComparisonOperator(ComparisonOperator.IN.toString()).
+ withAttributeValueList(attributeValues));
+ }
+ });
+ queryHandlers.put(Between.class, new QueryHandler<Between>() {
+ public void handle(PersistentEntity entity, Between criterion, Map<String, Condition> filter) {
+ String propertyName = criterion.getProperty();
+ String key = getKey(entity, propertyName);
+ String fromStringValue = DynamoDBConverterUtil.convertToString(criterion.getFrom(), entity.getMappingContext());
+ String toStringValue = DynamoDBConverterUtil.convertToString(criterion.getTo(), entity.getMappingContext());
+
+ DynamoDBUtil.checkFilterForExistingKey(filter, key);
+
+ boolean isNumber = DynamoDBConverterUtil.isNumber(criterion.getFrom());
+ Collection<AttributeValue> attributeValues = new ArrayList<AttributeValue>();
+ DynamoDBUtil.addAttributeValue(attributeValues, fromStringValue, isNumber);
+ DynamoDBUtil.addAttributeValue(attributeValues, toStringValue, isNumber);
+
+ filter.put(key, new Condition().withComparisonOperator(ComparisonOperator.BETWEEN.toString()).
+ withAttributeValueList(attributeValues));
+ }
+ });
+ queryHandlers.put(GreaterThan.class, new QueryHandler<GreaterThan>() {
+ public void handle(PersistentEntity entity, GreaterThan criterion, Map<String, Condition> filter) {
+ String propertyName = criterion.getProperty();
+ String key = getKey(entity, propertyName);
+ String stringValue = DynamoDBConverterUtil.convertToString(criterion.getValue(), entity.getMappingContext());
+ boolean isNumber = DynamoDBConverterUtil.isNumber(criterion.getValue());
+
+ DynamoDBUtil.addSimpleComparison(filter, key, ComparisonOperator.GT.toString(), stringValue, isNumber);
+ }
+ });
+ queryHandlers.put(GreaterThanEquals.class, new QueryHandler<GreaterThanEquals>() {
+ public void handle(PersistentEntity entity, GreaterThanEquals criterion, Map<String, Condition> filter) {
+ String propertyName = criterion.getProperty();
+ String key = getKey(entity, propertyName);
+ String stringValue = DynamoDBConverterUtil.convertToString(criterion.getValue(), entity.getMappingContext());
+ boolean isNumber = DynamoDBConverterUtil.isNumber(criterion.getValue());
+
+ DynamoDBUtil.addSimpleComparison(filter, key, ComparisonOperator.GE.toString(), stringValue, isNumber);
+ }
+ });
+ queryHandlers.put(LessThan.class, new QueryHandler<LessThan>() {
+ public void handle(PersistentEntity entity, LessThan criterion, Map<String, Condition> filter) {
+ String propertyName = criterion.getProperty();
+ String key = getKey(entity, propertyName);
+ String stringValue = DynamoDBConverterUtil.convertToString(criterion.getValue(), entity.getMappingContext());
+ boolean isNumber = DynamoDBConverterUtil.isNumber(criterion.getValue());
+
+ DynamoDBUtil.addSimpleComparison(filter, key, ComparisonOperator.LT.toString(), stringValue, isNumber);
+ }
+ });
+ queryHandlers.put(LessThanEquals.class, new QueryHandler<LessThanEquals>() {
+ public void handle(PersistentEntity entity, LessThanEquals criterion, Map<String, Condition> filter) {
+ String propertyName = criterion.getProperty();
+ String key = getKey(entity, propertyName);
+ String stringValue = DynamoDBConverterUtil.convertToString(criterion.getValue(), entity.getMappingContext());
+ boolean isNumber = DynamoDBConverterUtil.isNumber(criterion.getValue());
+
+ DynamoDBUtil.addSimpleComparison(filter, key, ComparisonOperator.LE.toString(), stringValue, isNumber);
+ }
+ });
+ }
+
+ public DynamoDBQuery(Session session, PersistentEntity entity, DynamoDBTableResolver tableResolver,
+ DynamoDBEntityPersister dynamoDBEntityPersister, DynamoDBTemplate dynamoDBTemplate) {
+ super(session, entity);
+ this.tableResolver = tableResolver;
+ this.dynamoDBEntityPersister = dynamoDBEntityPersister;
+ this.dynamoDBTemplate = dynamoDBTemplate;
+ }
+
+ @Override
+ protected List executeQuery(@SuppressWarnings("hiding") PersistentEntity entity, @SuppressWarnings("hiding") Junction criteria) {
+ String table = tableResolver.getAllTablesForEntity().get(0);
+
+ final List<Projection> projectionList = projections().getProjectionList();
+ boolean hasCountProjection = false;
+
+ if (!projectionList.isEmpty()) {
+ hasCountProjection = validateProjectionsAndCheckIfCountIsPresent(projectionList);
+ }
+
+ List<List<PropertyCriterion>> independentConditions = flattenAndReplaceDisjunction(criteria);
+ List<Map<String, Condition>> filters = buildFilters(independentConditions);
+
+ int maxToGet = max < 0 ? Integer.MAX_VALUE : max;
+ boolean hasOrdering = !getOrderBy().isEmpty();
+
+ List<Object> results;
+ if (projectionList.isEmpty()) {
+ if (!hasOrdering) {
+ results = doGetItems(table, filters, maxToGet).objects;
+ } else {
+ //since we sort in memory, if we have ordering we should get all matches, then sort, then cut to the maximum size
+ results = doGetItems(table, filters, Integer.MAX_VALUE).objects;
+ results = handleOrdering(entity, results);
+ results = resizeUpTo(results, maxToGet);
+ }
+ } else {
+ if (hasCountProjection) { //count is returned by AWS in a special way...
+ results = new ArrayList<Object>();
+ if (independentConditions.size() == 1) {
+ //optimization - if we only have one query to run we can use scan with the count request
+ int count = dynamoDBTemplate.scanCount(table, filters.get(0));
+ results.add(count);
+ } else {
+ //we have to actually get the items and return size of the collection because for queries '(a or b) and (a or c)' we can't return sum of counts
+ List<Object> loaded = doGetItems(table, filters, maxToGet).objects;
+ results.add(loaded.size());
+ }
+ } else {
+ List<Order> orderBys = getOrderBy();
+
+ if (!orderBys.isEmpty()) {
+ //too messy to implement for initial cut
+ throw new UnsupportedOperationException("'order by' can't be used with projections (not implemented yet). You have: " + orderBys.size());
+ }
+
+ List<Map<String, AttributeValue>> items = doGetItems(table, filters, maxToGet).items;
+ results = new ArrayList<Object>();
+ for (Projection projection : projectionList) {
+ if (IdProjection.class.equals(projection.getClass())) {
+ for (Map<String, AttributeValue> item : items) {
+ results.add(DynamoDBUtil.getAttributeValue(item, "id"));
+ }
+ } else if (PropertyProjection.class.equals(projection.getClass())) {
+ for (Map<String, AttributeValue> item : items) {
+ String key = extractPropertyKey(((PropertyProjection) projection).getPropertyName(), entity);
+ results.add(DynamoDBUtil.getAttributeValue(item, key));
+ }
+ }
+ }
+ }
+ }
+
+ return results;
+ }
+
+ private List<Object> resizeUpTo(List<Object> results, int max) {
+ if (results.size() > max) {
+ return results.subList(0, max);
+ } else {
+ return results;
+ }
+ }
+
+ private List<Object> handleOrdering(PersistentEntity entity, List<Object> results) {
+ ManualEntityOrdering ordering = new ManualEntityOrdering(entity);
+ List<Order> orderBys = getOrderBy();
+ results = ordering.applyOrder(results, orderBys);
+ return results;
+ }
+
+ private Results doGetItems(String table, List<Map<String, Condition>> filters, int maxToGet) {
+ List<Object> objects = new ArrayList<Object>();
+ List<Map<String, AttributeValue>> resultItems = new ArrayList<Map<String, AttributeValue>>();
+ Set alreadyLoadedIds = new HashSet();
+ for (Map<String, Condition> filter : filters) {
+ List<Map<String, AttributeValue>> items = dynamoDBTemplate.scan(table, filter, maxToGet);
+ for (Map<String, AttributeValue> item : items) {
+ Object id = DynamoDBUtil.getIdKey(item);
+ if (!alreadyLoadedIds.contains(id)) {
+ if (objects.size() < maxToGet) {
+ objects.add(createObjectFromItem(item));
+ resultItems.add(item);
+ alreadyLoadedIds.add(id);
+ }
+ }
+ }
+ }
+ return new Results(objects, resultItems);
+ }
+
+ private List<Map<String, Condition>> buildFilters(List<List<PropertyCriterion>> independentConditions) {
+ List<Map<String, Condition>> result = new ArrayList<Map<String, Condition>>();
+ if (independentConditions.isEmpty()) {
+ //if there are no criteria queries, create single dummy empty filter, otherwise we will not call dynamo at all
+ if (independentConditions.isEmpty()) {
+ result.add((Map<String, Condition>) Collections.EMPTY_MAP);
+ }
+ } else {
+ for (List<PropertyCriterion> query : independentConditions) {
+ Map<String, Condition> filter = new HashMap<String, Condition>();
+ for (PropertyCriterion propertyCriterion : query) {
+ QueryHandler queryHandler = queryHandlers.get(propertyCriterion.getClass());
+ if (queryHandler != null) {
+ queryHandler.handle(entity, propertyCriterion, filter);
+ } else {
+ throw new UnsupportedOperationException("Queries of type " +
+ propertyCriterion.getClass().getSimpleName() + " are not supported by this implementation");
+ }
+ }
+ result.add(filter);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * make sure that only property, id, or count projections are provided, and that the combination of them is meaningful.
+ * Throws exception if something is invalid.
+ *
+ * @param projections
+ * @returns true if count projection is present, false otherwise.
+ */
+ private boolean validateProjectionsAndCheckIfCountIsPresent(List<Projection> projections) {
+ //of the grouping projects AWS DynamoDB only supports count(*) projection, nothing else. Other kinds will have
+ //to be explicitly coded later...
+ boolean hasCountProjection = false;
+ for (Projection projection : projections) {
+ if (!(PropertyProjection.class.equals(projection.getClass()) ||
+ IdProjection.class.equals(projection.getClass()) ||
+ CountProjection.class.equals(projection.getClass())
+ )) {
+ throw new UnsupportedOperationException("Currently projections of type " +
+ projection.getClass().getSimpleName() + " are not supported by this implementation");
+ }
+
+
+ if (CountProjection.class.equals(projection.getClass())) {
+ hasCountProjection = true;
+ }
+ }
+ if (projections.size() > 1 && hasCountProjection) {
+ throw new IllegalArgumentException("Can not mix count projection and other types of projections. You requested: " + projections);
+ }
+ return hasCountProjection;
+ }
+
+ /**
+ * Recurses into the specified junction and flattens it into a list. Each top-level element in this list
+ * represents queries on properties (meant as conjunction queries) which must be fired independently of each other, and then later
+ * combined to get unique set of matching elements. This is needed because dynamodb does not support OR queries in any shape of form.
+ * For example, to illustrate behavior of this method:
+ * just property a ==> [ [a] ] //1 query
+ * a and b = con(a,b) ==> [ [a,b] ] //1 query
+ * a or b = dis(a,b) ==> [ [a], [b] ] //2 queries: 1 for a, 1 for b
+ * (a or b) and c = Con( Dis(a,b) , c ) = [Con(a,c), con(b,c)] ==> [ [a,c], [b,c] ] // 2 queries - 1 for a&c, 1 for b&c
+ * (a or b) and c,d = Con( Dis(a,b) , c , d) = [Con(a,c,d), con(b,c,d)] ==> [ [a,c,d], [b,c,d] ] //2 queries
+ * (a and b) and c = con(con(a,b), c) ==> [ [a,b,c] ] //1 query
+ * (a and b) or c = dis(con(a,b), c) ==> [ [a,b], [c] ] //2 queries
+ *
+ * @param criteria
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ private List<List<PropertyCriterion>> flattenAndReplaceDisjunction(@SuppressWarnings("hiding") Junction criteria) {
+ List<List<PropertyCriterion>> result = new ArrayList<List<PropertyCriterion>>();
+
+ if (criteria instanceof Conjunction) {
+ List<List<PropertyCriterion>> temp = handleConjunction((Conjunction) criteria);
+ result.addAll(temp);
+ } else if (criteria instanceof Disjunction) {
+ handleDisjunction(criteria, result);
+ } else if (criteria instanceof Negation) {
+ throw new RuntimeException("negation clause is not supported, please change your query");
+ } else {
+ throw new UnsupportedOperationException("Queries of type " +
+ criteria.getClass().getSimpleName() + " are not supported by this implementation");
+ }
+
+ return result;
+ }
+
+ private void handleDisjunction(Junction criteria, List<List<PropertyCriterion>> result) {
+ //we flatten each criterion and add output to result
+ for (Criterion c : criteria.getCriteria()) {
+ if (c instanceof PropertyCriterion) {
+ List<PropertyCriterion> temp = new ArrayList<PropertyCriterion>();
+ temp.add((PropertyCriterion) c);
+
+ result.add(temp);
+ } else {
+ List<List<PropertyCriterion>> flattened = flattenAndReplaceDisjunction((Junction) c);
+ result.addAll(flattened);
+ }
+ }
+ }
+
+ private List<List<PropertyCriterion>> handleConjunction(Conjunction criterion) {
+ List<List<PropertyCriterion>> result = new ArrayList<List<PropertyCriterion>>();
+ //first collect the non-disjunctions and disjunctions
+ List<PropertyCriterion> properties = new ArrayList<PropertyCriterion>();
+ List<List<List<PropertyCriterion>>> toCombinate = new ArrayList<List<List<PropertyCriterion>>>();
+ for (Criterion c : ((Conjunction) criterion).getCriteria()) {
+ if (c instanceof PropertyCriterion) {
+ properties.add((PropertyCriterion) c);
+ }
+ if (c instanceof Conjunction) {
+ //lets process it and see if there were any disjunctions internally
+ List<List<PropertyCriterion>> inner = flattenAndReplaceDisjunction((Junction) c);
+ if (inner.size() == 1) {
+ properties.addAll(inner.get(0));
+ } else {
+ toCombinate.add(inner);
+ }
+ }
+ if (c instanceof Disjunction) {
+ List<List<PropertyCriterion>> inner = flattenAndReplaceDisjunction((Junction) c);
+ //a or b = [ [a],[b] ]
+ toCombinate.add(inner);
+ }
+ if (c instanceof Negation) {
+ throw new RuntimeException("negation clause is not supported, please change your query");
+ }
+ }
+
+ if (toCombinate.isEmpty()) {
+ result.add(properties);
+ } else {
+ /*
+ con((a or b),(c or d),e) = con(a,c,e),con(b,c,e),con(a,d,e),con(b,d,e)
+ con((a or b),(c or d),e) =>
+ toCombinate is [
+ [ [a],[b] ],
+ [ [c],[d] ]
+ ]
+ properties = [ e ]
+ */
+ //add properties to the combination list as a single element, so it becomes
+ /*
+ toCombinate is [
+ [ [a],[b] ],
+ [ [c],[d] ],
+ [ [e] ]
+ ]
+ */
+ List<List<PropertyCriterion>> temp = new ArrayList<List<PropertyCriterion>>();
+ temp.add(properties);
+ toCombinate.add(temp);
+
+ List<List<PropertyCriterion>> combinations = DynamoDBUtil.combinate(toCombinate);
+ result = combinations;
+ }
+ return result;
+ }
+
+ protected Object createObjectFromItem(Map<String, AttributeValue> item) {
+ final String id = DynamoDBUtil.getAttributeValue(item, "id");
+ return dynamoDBEntityPersister.createObjectFromNativeEntry(getEntity(), id,
+ new DynamoDBNativeItem(item));
+ }
+
+ protected static interface QueryHandler<T> {
+ public void handle(PersistentEntity entity, T criterion, Map<String, Condition> filter);
+ }
+
+ protected static String extractPropertyKey(String propertyName, PersistentEntity entity) {
+ PersistentProperty prop = null;
+ if (entity.isIdentityName(propertyName)) {
+ prop = entity.getIdentity();
+ } else {
+ prop = entity.getPropertyByName(propertyName);
+ }
+
+ if (prop == null) {
+ throw new IllegalArgumentException(
+ "Could not find property '" + propertyName + "' in entity '" + entity.getName() + "' : " + entity);
+ }
+
+ KeyValue kv = (KeyValue) prop.getMapping().getMappedForm();
+ String key = kv.getKey();
+ return key;
+ }
+
+
+ /**
+ * Returns mapped key
+ *
+ * @param entity
+ * @param propertyName
+ * @return
+ */
+ protected static String getKey(PersistentEntity entity, String propertyName) {
+ return extractPropertyKey(propertyName, entity);
+ }
+
+ /**
+ * simple temp container
+ */
+ protected static class Results {
+ public Results(List<Object> objects, List<Map<String, AttributeValue>> items) {
+ this.objects = objects;
+ this.items = items;
+ }
+
+ public List<Object> objects;
+ public List<Map<String, AttributeValue>> items;
+ }
+}
View
11 ...ore-dynamodb/src/main/groovy/org/grails/datastore/mapping/dynamodb/util/DataStoreOperationException.java