DATACOUCH-275 Key auto generation using Attributes, UUID

Original pull request: #142.
This commit is contained in:
Subhashni Balakrishnan
2017-04-12 14:38:21 -07:00
parent 766e4eeebd
commit 069ceea65c
14 changed files with 797 additions and 25 deletions

View File

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

View File

@@ -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<Boolean>() {
@Override
public Boolean doInBucket() throws InterruptedException, ExecutionException {
converted.setId(addCommonPrefixAndSuffix(converted.getId()));
Document<String> doc = encodeAndWrap(converted, version);
Document<String> 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;
}
}

View File

@@ -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<CouchbasePersistentProperty> idProperty = entity.getIdProperty();
final Optional<CouchbasePersistentProperty> 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<Integer, String> prefixes = new TreeMap<>();
final TreeMap<Integer, String> suffixes = new TreeMap<>();
final TreeMap<Integer, String> 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<Object> 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<Integer, String> prefixes, TreeMap<Integer, String> suffixes,
TreeMap<Integer, String> 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<String> values, String delimiter) {
boolean isAppending = false;
for(String value : values) {
if (isAppending) {
sb.append(delimiter);
} else {
isAppending = true;
}
sb.append(value);
}
return sb;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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