From 069ceea65c42bcf8ca3f845781363681878a8927 Mon Sep 17 00:00:00 2001 From: Subhashni Balakrishnan Date: Wed, 12 Apr 2017 14:38:21 -0700 Subject: [PATCH] DATACOUCH-275 Key auto generation using Attributes, UUID Original pull request: #142. --- .../IntegrationTestCustomKeySettings.java | 7 + .../CouchbaseTemplateIdGenerationTests.java | 147 ++++++++++++++++++ .../CouchbaseTemplateKeySettingsTests.java | 63 ++++++++ src/main/asciidoc/autokeygeneration.adoc | 106 +++++++++++++ src/main/asciidoc/index.adoc | 1 + .../couchbase/core/CouchbaseOperations.java | 28 ++++ .../couchbase/core/CouchbaseTemplate.java | 64 ++++++-- .../convert/MappingCouchbaseConverter.java | 131 ++++++++++++++-- .../couchbase/core/mapping/KeySettings.java | 92 +++++++++++ .../core/mapping/id/GeneratedValue.java | 39 +++++ .../core/mapping/id/GenerationStrategy.java | 34 ++++ .../core/mapping/id/IdAttribute.java | 35 +++++ .../couchbase/core/mapping/id/IdPrefix.java | 37 +++++ .../couchbase/core/mapping/id/IdSuffix.java | 38 +++++ 14 files changed, 797 insertions(+), 25 deletions(-) create mode 100644 src/integration/java/org/springframework/data/couchbase/IntegrationTestCustomKeySettings.java create mode 100644 src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateIdGenerationTests.java create mode 100644 src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeySettingsTests.java create mode 100644 src/main/asciidoc/autokeygeneration.adoc create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/KeySettings.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/id/GeneratedValue.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/id/GenerationStrategy.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/id/IdAttribute.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/id/IdPrefix.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/id/IdSuffix.java diff --git a/src/integration/java/org/springframework/data/couchbase/IntegrationTestCustomKeySettings.java b/src/integration/java/org/springframework/data/couchbase/IntegrationTestCustomKeySettings.java new file mode 100644 index 00000000..9ee5d5df --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/IntegrationTestCustomKeySettings.java @@ -0,0 +1,7 @@ +package org.springframework.data.couchbase; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class IntegrationTestCustomKeySettings extends IntegrationTestApplicationConfig { +} diff --git a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateIdGenerationTests.java b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateIdGenerationTests.java new file mode 100644 index 00000000..ec32b23a --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateIdGenerationTests.java @@ -0,0 +1,147 @@ +package org.springframework.data.couchbase.core; + +import static org.junit.Assert.*; +import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.*; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.error.DocumentDoesNotExistException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.IntegrationTestApplicationConfig; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.id.GeneratedValue; +import org.springframework.data.couchbase.core.mapping.id.IdAttribute; +import org.springframework.data.couchbase.core.mapping.id.IdPrefix; +import org.springframework.data.couchbase.core.mapping.id.IdSuffix; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Subhashni Balakrishnan + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = IntegrationTestApplicationConfig.class) +public class CouchbaseTemplateIdGenerationTests { + + @Rule + public TestName testName = new TestName(); + + @Autowired + private Bucket client; + + @Autowired + private CouchbaseTemplate template; + + + private void removeIfExist(String key) { + try { + client.remove(key); + } + catch (DocumentDoesNotExistException e) { + //ignore + } + } + + @Test + public void shouldGenerateIdUsingAtrributes() throws Exception { + SimpleClassWithGeneratedIdValueUsingAttributes simpleClass = new SimpleClassWithGeneratedIdValueUsingAttributes(); + String generatedId = template.getGeneratedId(simpleClass); + + removeIfExist(generatedId); + assertEquals("Id generation should be correct", generatedId, + "prefix1::prefix2::0::1::2.0::3.0::4::Simple::Nested{value:simple}::suffix1::suffix2"); + template.insert(simpleClass); + assertEquals("Exists after insert", true, template.exists(generatedId)); + simpleClass.value = "modified"; + template.save(simpleClass); + SimpleClassWithGeneratedIdValueUsingAttributes modifiedClass = template.findById(generatedId, + SimpleClassWithGeneratedIdValueUsingAttributes.class); + assertEquals("Get after save id should be correct", generatedId, modifiedClass.id); + template.update(simpleClass); + SimpleClassWithGeneratedIdValueUsingAttributes updatedClass = template.findById(generatedId, + SimpleClassWithGeneratedIdValueUsingAttributes.class); + assertEquals("Get after update id should be correct", generatedId, updatedClass.id); + template.remove(generatedId); + assertEquals("Exists after remove", false, template.exists(generatedId)); + } + + @Test + public void shouldGenerateIdUsingUUID() throws Exception { + SimpleClassWithGeneratedIdValueUsingUUID simpleClass = new SimpleClassWithGeneratedIdValueUsingUUID(); + String generatedId = template.getGeneratedId(simpleClass); + simpleClass.id = generatedId; + template.insert(simpleClass); + assertEquals("Should not regenerate id", generatedId, simpleClass.id); + template.remove(generatedId); + assertEquals("Exists after remove", false, template.exists(generatedId)); + } + + + @Document + static class SimpleClassWithGeneratedIdValueUsingAttributes { + + @Id @GeneratedValue(strategy = USE_ATTRIBUTES, delimiter = "::") + public String id; + + @IdAttribute(order = 6) + public Nested nested = new Nested("simple"); + + @IdAttribute(order = 5) + public String type = "Simple"; + + @IdAttribute(order = 4) + public int intNum = 4; + + @IdAttribute(order = 2) + public float floatNum = 2F; + + @IdAttribute(order = 3) + public double doubleNum = 3; + + @IdAttribute + public long longNum = 0L; + + @IdAttribute(order = 1) + public short shortNum = 1; + + @IdPrefix(order = 1) + public String prefix2 = "prefix2"; + + @IdPrefix + public String prefix1 = "prefix1"; + + @IdSuffix(order = 1) + public String suffix2 = "suffix2"; + + @IdSuffix + public String suffix1 = "suffix1"; + + public String value = "new"; + + } + + static class Nested { + private String value; + + public Nested(String value) { + this.value = value; + } + + @Override + public String toString() { + return "Nested{value:" + value + "}"; + } + } + + @Document + static class SimpleClassWithGeneratedIdValueUsingUUID { + @Id @GeneratedValue(strategy = UNIQUE) + public String id; + + public String value = "new"; + } +} \ No newline at end of file diff --git a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeySettingsTests.java b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeySettingsTests.java new file mode 100644 index 00000000..2b267f9d --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeySettingsTests.java @@ -0,0 +1,63 @@ +package org.springframework.data.couchbase.core; + +import static org.junit.Assert.*; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.error.DocumentDoesNotExistException; +import com.couchbase.client.java.repository.annotation.Id; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.couchbase.IntegrationTestCustomKeySettings; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.KeySettings; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Subhashni Balakrishnan + */ + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = IntegrationTestCustomKeySettings.class) +public class CouchbaseTemplateKeySettingsTests { + + @Rule + public TestName testName = new TestName(); + + @Autowired + private CouchbaseTemplate template; + + @Before + public void setup() { + if (template.keySettings() == null) { + template.keySettings(KeySettings.build().prefix("MyAppPrefix").suffix("MyAppSuffix").delimiter("::")); + } + } + + @Test + public void shouldAddCustomKeySettings() throws Exception { + SimpleClass simpleClass = new SimpleClass(); + String generatedId = template.getGeneratedId(simpleClass); + assertEquals("Id generated should include custom key settings", "MyAppPrefix::myId::MyAppSuffix", generatedId); + } + + @Test + public void shouldNotAllowKeySettingsToBeChanged() { + try { + template.keySettings(KeySettings.build().prefix("MyAppPrefix").suffix("MyAppSuffix").delimiter("::")); + fail("excepted unsupportedOperationException"); + } catch(Exception ex) { + + } + } + + @Document + static class SimpleClass { + @Id + public String id = "myId"; + } +} \ No newline at end of file diff --git a/src/main/asciidoc/autokeygeneration.adoc b/src/main/asciidoc/autokeygeneration.adoc new file mode 100644 index 00000000..6998c2d7 --- /dev/null +++ b/src/main/asciidoc/autokeygeneration.adoc @@ -0,0 +1,106 @@ +[[couchbase.autokeygeneration]] += Auto generating keys + +This chapter describes how couchbase document keys can be auto-generated using builtin mechanisms. +There are two types of auto-generation strategies supported. + + - <> + - <> + +NOTE: The maximum key length supported by couchbase is 250 bytes. + +[[couchbase.autokeygeneration.configuration]] +== Configuration + +Keys to be auto-generated should be annotated with `@GeneratedValue`. The default strategy is `USE_ATTRIBUTES`. Prefix +and suffix for the key can be provided as part of the entity itself, these values are not persisted, they are only +used for key generation. The prefixes and suffixes are ordered using the `order` value. The default order is `0`, multiple +prefixes without order will overwrite the previous. If a value for id is already available, auto-generation will be skipped. +The delimiter for concatenation can be provided using `delimiter`, the default delimiter is `.`. + +.Annotation for GeneratedValue +==== +[source,java] +---- +@Document +public class User { + @Id @GeneratedValue(strategy = USE_ATTRIBUTES, delimiter = ".") + private String id; + @IdPrefix(order=0) + private String userPrefix; + @IdSuffix(order=0) + private String userSuffix; + ... +} +---- +==== + +Common prefix and suffix for all entities keys can be added to `CouchbaseTemplate` directly. Once added to the `CouchbaseTemplate`, +they become immutable. These settings are always applied irrespective of the `GeneratedValue` annotation. + +.Common key settings in CouchbaseTemplate +==== +[source,java] +---- +@Autowired +CouchbaseTemplate couchbaseTemplate; +... +couchbaseTemplate.keySettings(KeySettings.build().prefix("ApplicationA").suffix("Server1").delimiter("::")); +---- +==== + +Key will be auto-generated only for operations with direct entity input like insert, update, save, delete using entity. +For other operations requiring just the key, it can be generated using `CouchbaseTemplate`. + +.Standalone key generation in CouchbaseTemplate +==== +[source,java] +---- +@Autowired +CouchbaseTemplate couchbaseTemplate; +... +String id = couchbaseTemplate.getGeneratedId(entity); +... +repo.exists(id); +---- +==== + +[[couchbase.autokeygeneration.usingattributes]] +== Key generation using attributes + +It is a common practice to generate keys using a combination of the document attributes. Key generation using attributes +concatenates all the attribute values annotated with `IdAttribute`, based on the ordering provided similar to prefixes and suffixes. + +.Annotation for IdAttribute +==== +[source,java] +---- +@Document +public class User { + @Id @GeneratedValue(strategy = USE_ATTRIBUTES) + private String id; + @IdAttribute + private String userid; + ... +} +---- +==== + +[[couchbase.autokeygeneration.unique]] +== Key generation using uuid + +This auto-generation uses UUID random generator to generate document keys consuming 16 bytes of key space. This mechanism +is only recommended for test scaffolding. + +.Annotation for Unique key generation +==== +[source,java] +---- +@Document +public class User { + @Id @GeneratedValue(strategy = UNIQUE) + private String id; + ... +} +---- +==== \ No newline at end of file diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index dc9aa148..e8d04262 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -20,6 +20,7 @@ include::preface.adoc[] :leveloffset: +1 include::configuration.adoc[] include::entity.adoc[] +include::autokeygeneration.adoc[] include::{spring-data-commons-docs}/repositories.adoc[] include::repository.adoc[] include::template.adoc[] diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java index df26a170..9f2569cd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -35,6 +35,7 @@ import com.couchbase.client.java.view.ViewResult; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.KeySettings; import org.springframework.data.couchbase.core.query.Consistency; @@ -384,4 +385,31 @@ public interface CouchbaseOperations { * @return the consistency to use for generated repository queries. */ Consistency getDefaultConsistency(); + + /** + * Add common key settings + * + * Throws {@link UnsupportedOperationException} if KeySettings is already set. It becomes immutable. + * + * @param settings {@link KeySettings} + */ + void keySettings(final KeySettings settings); + + /** + * Get key settings associated with the template + * + * @return {@link KeySettings} + */ + KeySettings keySettings(); + + /** + * Get generated id - applies both using prefix and suffix through entity as well as template {@link KeySettings} + * + * ** NOTE: UNIQUE strategy will generate different ids each time *** + * + * @param entity the entity object. + * @return id the couchbase document key. + */ + String getGeneratedId(Object entity); + } \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 1a657c20..67fe81e4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -51,6 +51,12 @@ import com.couchbase.client.java.view.ViewQuery; import com.couchbase.client.java.view.ViewResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.CouchbaseStorable; +import org.springframework.data.couchbase.core.mapping.KeySettings; import rx.Observable; import rx.functions.Func1; @@ -63,11 +69,6 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; -import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; -import org.springframework.data.couchbase.core.mapping.CouchbaseStorable; import org.springframework.data.couchbase.core.mapping.event.AfterDeleteEvent; import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent; import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent; @@ -107,6 +108,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP private final CouchbaseConverter converter; private final TranslationService translationService; private final ClusterInfo clusterInfo; + private KeySettings keySettings; private ApplicationEventPublisher eventPublisher; @@ -126,7 +128,8 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP this(clusterInfo, client, null, translationService); } - public CouchbaseTemplate(final ClusterInfo clusterInfo, final Bucket client, + public CouchbaseTemplate(final ClusterInfo clusterInfo, + final Bucket client, final CouchbaseConverter converter, final TranslationService translationService) { this.clusterInfo = clusterInfo; @@ -134,8 +137,10 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP this.converter = converter == null ? getDefaultConverter() : converter; this.translationService = translationService == null ? getDefaultTranslationService() : translationService; this.mappingContext = this.converter.getMappingContext(); + } + private TranslationService getDefaultTranslationService() { JacksonTranslationService t = new JacksonTranslationService(); t.afterPropertiesSet(); @@ -587,12 +592,14 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP execute(new BucketCallback() { @Override public Boolean doInBucket() throws InterruptedException, ExecutionException { + converted.setId(addCommonPrefixAndSuffix(converted.getId())); Document doc = encodeAndWrap(converted, version); Document storedDoc; //We will check version only if required boolean versionPresent = versionProperty.isPresent(); //If version is not set - assumption that document is new, otherwise updating boolean existingDocument = version != null && version > 0L; + try { switch (persistType) { case SAVE: @@ -646,8 +653,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP @Override public Boolean doInBucket() throws InterruptedException, ExecutionException { try { - RawJsonDocument deletedDoc = client.remove((String) objectToRemove, persistTo, replicateTo, - RawJsonDocument.class); + RawJsonDocument deletedDoc = client.remove((String) objectToRemove , persistTo, replicateTo, RawJsonDocument.class); return deletedDoc != null; } catch (Exception e) { handleWriteResultError("Delete document failed: " + e.getMessage(), e); @@ -666,7 +672,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP @Override public Boolean doInBucket() { try { - RawJsonDocument deletedDoc = client.remove(converted.getId(), persistTo, replicateTo + RawJsonDocument deletedDoc = client.remove(addCommonPrefixAndSuffix(converted.getId()), persistTo, replicateTo , RawJsonDocument.class); return deletedDoc != null; } catch (Exception e) { @@ -752,4 +758,42 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP return springDataOperationName; } } -} + + @Override + public void keySettings(KeySettings settings) { + if (this.keySettings != null) { + throw new UnsupportedOperationException("Key settings is already set, it is no longer mutable"); + } + this.keySettings = settings; + } + + @Override + public KeySettings keySettings() { + return this.keySettings; + } + + @Override + public String getGeneratedId(Object entity) { + ensureNotIterable(entity); + CouchbaseDocument converted = new CouchbaseDocument(); + converter.write(entity, converted); + return addCommonPrefixAndSuffix(converted.getId()); + } + + private String addCommonPrefixAndSuffix(final String id) { + String convertedKey = id; + if (this.keySettings == null) { + return id; + } + String prefix = this.keySettings.prefix(); + String delimiter = this.keySettings.delimiter(); + String suffix = this.keySettings.suffix(); + if (prefix != null && !prefix.equals("")) { + convertedKey = prefix + delimiter + convertedKey; + } + if (suffix != null && !suffix.equals("")) { + convertedKey = convertedKey + delimiter + suffix; + } + return convertedKey; + } +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index ffeb483d..57955235 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.core.convert; +import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.*; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -23,6 +24,8 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.TreeMap; +import java.util.UUID; import com.couchbase.client.java.repository.annotation.Field; @@ -36,6 +39,10 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbaseList; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.id.GeneratedValue; +import org.springframework.data.couchbase.core.mapping.id.IdAttribute; +import org.springframework.data.couchbase.core.mapping.id.IdPrefix; +import org.springframework.data.couchbase.core.mapping.id.IdSuffix; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -206,7 +213,12 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter return read(entity, source, parent); } - /** + private boolean isIdConstructionProperty(final CouchbasePersistentProperty property) { + return property.isAnnotationPresent(IdPrefix.class) || property.isAnnotationPresent(IdSuffix.class); + } + + + /** * Read an incoming {@link CouchbaseDocument} into the target entity. * * @param entity the target entity. @@ -226,10 +238,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter entity.getPersistentProperties().filter(prop -> { - if (!(prop.isIdProperty() || source.containsKey(prop.getFieldName())) || entity.isConstructorArgument(prop)) { + if (!(prop.isIdProperty() || source.containsKey(prop.getFieldName())) || entity.isConstructorArgument(prop) || isIdConstructionProperty(prop)) { return false; } - return true; }).forEach(prop -> { @@ -366,7 +377,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter writeInternal(source, target, type); if (target.getId() == null) { - throw new MappingException("An ID property is needed, but not found on this entity."); + throw new MappingException("An ID property is needed, but not found/could not be generated on this entity."); } } @@ -417,6 +428,16 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter target.setExpiration(source.getExpiration()); } + private String convertToString(Object propertyObj) { + if (propertyObj instanceof String) { + return (String) propertyObj; + } else if (propertyObj instanceof Number) { + return new StringBuffer().append(propertyObj).toString(); + } else { + return propertyObj.toString(); + } + } + /** * Internal helper method to write the source object into the target document. * @@ -438,19 +459,17 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter final Optional idProperty = entity.getIdProperty(); final Optional versionProperty = entity.getVersionProperty(); - if (target.getId() == null) { - idProperty.ifPresent(id -> { - target.setId(accessor.getProperty(id, String.class).orElse(null)); - }); - } - target.setExpiration(entity.getExpiry()); + final TreeMap prefixes = new TreeMap<>(); + final TreeMap suffixes = new TreeMap<>(); + final TreeMap idAttributes = new TreeMap<>(); + + target.setExpiration(entity.getExpiry()); entity.getPersistentProperties().filter(prop -> { if (idProperty.filter(prop::equals).isPresent()) { return false; } - if (versionProperty.filter(prop::equals).isPresent()) { return false; } @@ -462,14 +481,51 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter }).forEach(prop -> { Optional propertyObj = accessor.getProperty(prop, (Class) prop.getType()); propertyObj.ifPresent(o -> { - if (!conversions.isSimpleType(o.getClass())) { - writePropertyInternal(o, target, prop); - } else { - writeSimpleInternal(o, target, prop.getFieldName()); + if (null != o) { + if (prop.isAnnotationPresent(IdPrefix.class)) { + prop.findAnnotation(IdPrefix.class).ifPresent(p -> { + int order = p.order(); + prefixes.put(order, convertToString(o)); + }); + return; + } + + if (prop.isAnnotationPresent(IdSuffix.class)) { + prop.findAnnotation(IdSuffix.class).ifPresent(p -> { + int order = p.order(); + suffixes.put(order, convertToString(o)); + }); + return; + } + + if (prop.isAnnotationPresent(IdAttribute.class)) { + prop.findAnnotation(IdAttribute.class).ifPresent(p -> { + int order = p.order(); + idAttributes.put(order, convertToString(o)); + }); + } + + if (!conversions.isSimpleType(o.getClass())) { + writePropertyInternal(o, target, prop); + } else { + writeSimpleInternal(o, target, prop.getFieldName()); + } } }); }); + if (target.getId() == null) { + idProperty.ifPresent(prop -> { + String id = accessor.getProperty(prop, String.class).orElse(null); + if(prop.isAnnotationPresent(GeneratedValue.class) && (id == null || id.equals(""))) { + prop.findAnnotation(GeneratedValue.class).ifPresent(generatedValueInfo -> { + target.setId(generateId(generatedValueInfo, prefixes, suffixes, idAttributes)); + }); + } else { + target.setId(id); + } + }); + } entity.getAssociations().forEach(association -> { CouchbasePersistentProperty inverseProp = association.getInverse(); @@ -835,4 +891,49 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter } } + private String generateId(GeneratedValue generatedValue, TreeMap prefixes, TreeMap suffixes, + TreeMap idAttributes) { + String delimiter = generatedValue.delimiter(); + StringBuilder sb = new StringBuilder(); + boolean isAppending = false; + if (prefixes.size() > 0) { + appendKeyParts(sb, prefixes.values(), delimiter); + isAppending = true; + } + + if (generatedValue.strategy() == USE_ATTRIBUTES && idAttributes.size() > 0) { + if(isAppending) { + sb.append(delimiter); + } + appendKeyParts(sb, idAttributes.values(), delimiter); + } + + if (generatedValue.strategy() == UNIQUE) { + if(isAppending) { + sb.append(delimiter); + } + sb.append(UUID.randomUUID()); + } + + if (suffixes.size() > 0) { + if(isAppending) { + sb.append(delimiter); + } + appendKeyParts(sb, suffixes.values(), delimiter); + } + return sb.toString(); + } + + private StringBuilder appendKeyParts(StringBuilder sb, Collection values, String delimiter) { + boolean isAppending = false; + for(String value : values) { + if (isAppending) { + sb.append(delimiter); + } else { + isAppending = true; + } + sb.append(value); + } + return sb; + } } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/KeySettings.java b/src/main/java/org/springframework/data/couchbase/core/mapping/KeySettings.java new file mode 100644 index 00000000..e9c3c44f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/KeySettings.java @@ -0,0 +1,92 @@ +/* + * Copyright 2017 the original author or authors + * + * 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.springframework.data.couchbase.core.mapping; + +/** + * Common settings for Couchbase key + * - prefix + * - suffix + * - delimiter + * + * @author Subhashni Balakrishnan + */ +public class KeySettings { + + private static String DEFAULT_DELIMITER = "."; + + private String commonPrefix; + + private String commonSuffix; + + private String delimiter; + + public static KeySettings build() { + return new KeySettings(); + } + + protected KeySettings() { + this.delimiter = DEFAULT_DELIMITER; + } + + /** + * Set common prefix + */ + public KeySettings prefix(String prefix) { + this.commonPrefix = prefix; + return this; + } + + /** + * Set common suffix + */ + public KeySettings suffix(String suffix) { + this.commonSuffix = suffix; + return this; + } + + /** + * Set delimiter + */ + public KeySettings delimiter(String delimiter) { + if (delimiter == null) { + return this; + } + this.delimiter = delimiter; + return this; + } + + /** + * Get common prefix + */ + public String prefix() { + return this.commonPrefix; + } + + /** + * Get common suffix + */ + public String suffix() { + return this.commonSuffix; + } + + /** + * Get delimiter + */ + public String delimiter() { + return this.delimiter; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/GeneratedValue.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/GeneratedValue.java new file mode 100644 index 00000000..f4ef4b64 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/GeneratedValue.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017 the original author or authors + * + * 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.springframework.data.couchbase.core.mapping.id; +import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.*; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.data.couchbase.core.mapping.Document; + +/** + * This annotation is targeted at {@link Document} interfaces, indicating that + * the framework should generate the document key value using the specified generator + * + * @author Subhashni Balakrishnan + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface GeneratedValue { + + String delimiter() default "."; + + GenerationStrategy strategy() default USE_ATTRIBUTES; +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/GenerationStrategy.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/GenerationStrategy.java new file mode 100644 index 00000000..c5a53020 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/GenerationStrategy.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 the original author or authors + * + * 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.springframework.data.couchbase.core.mapping.id; + +/** + * Keys generation strategy + * + * @author Subhashni Balakrishnan + */ +public enum GenerationStrategy { + /** + * Constructs key from the entity attributes using the supplied id, prefix, suffix + */ + USE_ATTRIBUTES, + + /** + * Uses an uuid generator + */ + UNIQUE +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdAttribute.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdAttribute.java new file mode 100644 index 00000000..1e6c222c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdAttribute.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 the original author or authors + * + * 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.springframework.data.couchbase.core.mapping.id; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Inherited; + +/** + * This annotation is targeted at building the document id using the attribute value. + * + * @author Subhashni Balakrishnan + */ +@Inherited +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface IdAttribute { + int order() default 0; +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdPrefix.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdPrefix.java new file mode 100644 index 00000000..d51b6e43 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdPrefix.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 the original author or authors + * + * 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.springframework.data.couchbase.core.mapping.id; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Inherited; + +/** + * This annotation is targeted at building the document id using the value + * as a part of a prefix build. The order determines in way which the specified + * prefix value is used in building the prefix. + * + * @author Subhashni Balakrishnan + */ +@Inherited +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface IdPrefix { + int order() default 0; +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdSuffix.java b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdSuffix.java new file mode 100644 index 00000000..4114a69b --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/id/IdSuffix.java @@ -0,0 +1,38 @@ +/* + * Copyright 2017 the original author or authors + * + * 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.springframework.data.couchbase.core.mapping.id; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Inherited; + +/** + * This annotation is targeted at building the document id using the value + * as a part of a suffix build. The order determines in way which the specified + * suffix value is used in building the suffix. + * + * @author Subhashni Balakrishnan + */ +@Inherited +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface IdSuffix { + int order() default 0; +} \ No newline at end of file