From 95c69ea8ccb8c09f5e20b089e7bfe565c11add18 Mon Sep 17 00:00:00 2001 From: mikereiche Date: Tue, 2 Jun 2020 15:59:57 -0700 Subject: [PATCH] DATACOUCH-525 - support custom type field name and custome type field value MOTIVATION customers would like to specify a type field other than _class, and would like to specify a type field value other than the class name of the domain object CHANGES Follow the instructions at https://stackoverflow.com/questions/38847605/how-to-use-spring-data-with-couchbase-without-class-attribute in the test domain, supply a CustomMappingCouchbaseConvert class that leverates a TypeBasedCouchbaseTypeMapper, which in turn uses a TypeAwareTypeInformationMapper. The typeKey used is "t". The TypeAwareTypeInformationMapper uses the DocumentType annotation as the value for the type map entry. The domain Person object has @DocumentType("p"), which will result in type mapping of "t" : "p" for Person objects. In addition, when a query is composed, the typeKey and typeValue are taken from the converter. --- .../core/convert/CouchbaseConverter.java | 7 ++ .../core/convert/CouchbaseTypeMapper.java | 5 ++ .../convert/DefaultCouchbaseTypeMapper.java | 5 ++ .../convert/MappingCouchbaseConverter.java | 12 ++-- .../data/couchbase/core/query/Query.java | 29 ++++---- .../MappingCouchbaseConverterTests.java | 70 +++++++++++++++++++ .../data/couchbase/domain/Config.java | 38 ++++++++++ .../CustomMappingCouchbaseConverter.java | 23 ++++++ .../data/couchbase/domain/Person.java | 14 ++-- .../TypeAwareTypeInformationMapper.java | 20 ++++++ .../domain/TypeBasedCouchbaseTypeMapper.java | 31 ++++++++ 11 files changed, 231 insertions(+), 23 deletions(-) create mode 100644 src/test/java/org/springframework/data/couchbase/domain/CustomMappingCouchbaseConverter.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/TypeAwareTypeInformationMapper.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/TypeBasedCouchbaseTypeMapper.java diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java index ca452bff..ee7a68cc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java @@ -21,6 +21,8 @@ import org.springframework.data.convert.EntityReader; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.mapping.Alias; +import org.springframework.data.util.TypeInformation; /** * Marker interface for the converter, identifying the types to and from that can be converted. @@ -54,4 +56,9 @@ public interface CouchbaseConverter * @return the name of the field that will hold type information. */ String getTypeKey(); + + /** + * @return the alias value for the type + */ + Alias getTypeAlias(TypeInformation info); } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java index 31e8a059..7ea67003 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java @@ -18,14 +18,19 @@ package org.springframework.data.couchbase.core.convert; import org.springframework.data.convert.TypeMapper; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.mapping.Alias; +import org.springframework.data.util.TypeInformation; /** * Marker interface for the TypeMapper. * * @author Michael Nitschinger + * @author Michael Reiche */ public interface CouchbaseTypeMapper extends TypeMapper { String getTypeKey(); + Alias getTypeAlias(TypeInformation info); + } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java b/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java index 5a16f162..3600044e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java @@ -20,6 +20,7 @@ import org.springframework.data.convert.DefaultTypeMapper; import org.springframework.data.convert.TypeAliasAccessor; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.mapping.Alias; +import org.springframework.data.util.TypeInformation; /** * The Couchbase Type Mapper. @@ -72,4 +73,8 @@ public class DefaultCouchbaseTypeMapper extends DefaultTypeMapper info) { + return getAliasFor(info); + } } 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 6f5043be..52d9189d 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 @@ -39,17 +39,14 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseList; 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.event.AfterConvertCallback; 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.couchbase.core.query.N1qlJoin; -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.AssociationHandler; -import org.springframework.data.mapping.MappingException; -import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.*; import org.springframework.data.mapping.PreferredConstructor.Parameter; -import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -182,6 +179,11 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem return typeMapper.getTypeKey(); } + @Override + public Alias getTypeAlias(TypeInformation info) { + return typeMapper.getTypeAlias(info); + } + @Override public R read(final Class clazz, final CouchbaseDocument source) { return read(ClassTypeInformation.from(clazz), source, null); diff --git a/src/main/java/org/springframework/data/couchbase/core/query/Query.java b/src/main/java/org/springframework/data/couchbase/core/query/Query.java index 1d57d85e..feda684f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/Query.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/Query.java @@ -31,6 +31,9 @@ import org.springframework.data.couchbase.repository.query.StringBasedN1qlQueryP import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.mapping.Alias; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; /** @@ -47,8 +50,7 @@ public class Query { static private final Pattern WHERE_PATTERN = Pattern.compile("\\sWHERE\\s"); - public Query() { - } + public Query() {} public Query(final QueryCriteria criteriaDefinition) { addCriteria(criteriaDefinition); @@ -60,8 +62,8 @@ public class Query { } /** - * set the postional parameters on the query object - * There can only be named parameters or positional parameters - not both. + * set the postional parameters on the query object There can only be named parameters or positional parameters - not + * both. * * @param parameters - the positional parameters * @return - the query @@ -72,8 +74,8 @@ public class Query { } /** - * set the named parameters on the query object - * There can only be named parameters or positional parameters - not both. + * set the named parameters on the query object There can only be named parameters or positional parameters - not + * both. * * @param parameters - the named parameters * @return - the query @@ -157,10 +159,8 @@ public class Query { sb.append(" ORDER BY "); sort.stream().forEach(order -> { if (order.isIgnoreCase()) { - throw new IllegalArgumentException(String.format( - "Given sort contained an Order for %s with ignore case! " - + "Couchbase N1QL does not support sorting ignoring case currently!", - order.getProperty())); + throw new IllegalArgumentException(String.format("Given sort contained an Order for %s with ignore case! " + + "Couchbase N1QL does not support sorting ignoring case currently!", order.getProperty())); } sb.append(order.getProperty()).append(" ").append(order.isAscending() ? "ASC," : "DESC,"); }); @@ -257,10 +257,15 @@ public class Query { StringBasedN1qlQueryParser.N1qlSpelValues getN1qlSpelValues(ReactiveCouchbaseTemplate template, Class domainClass, boolean isCount) { String typeKey = template.getConverter().getTypeKey(); - final CouchbasePersistentEntity persistentEntity = template.getConverter().getMappingContext().getRequiredPersistentEntity( - domainClass); + final CouchbasePersistentEntity persistentEntity = template.getConverter().getMappingContext() + .getRequiredPersistentEntity(domainClass); MappingCouchbaseEntityInformation info = new MappingCouchbaseEntityInformation<>(persistentEntity); String typeValue = info.getJavaType().getName(); + TypeInformation typeInfo = ClassTypeInformation.from(info.getJavaType()); + Alias alias = template.getConverter().getTypeAlias(typeInfo); + if (alias != null && alias.isPresent()) { + typeValue = alias.toString(); + } return StringBasedN1qlQueryParser.createN1qlSpelValues(template.getBucketName(), typeKey, typeValue, isCount); } diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java index 906908a8..2a2a14b8 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java @@ -28,11 +28,13 @@ import java.util.*; import org.junit.jupiter.api.Test; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.TypeAlias; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; import org.springframework.data.couchbase.core.convert.CouchbaseJsr310Converters.LocalDateTimeToLongConverter; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.domain.Config; import org.springframework.data.couchbase.domain.User; import org.springframework.data.mapping.MappingException; @@ -45,9 +47,11 @@ import org.springframework.data.mapping.MappingException; public class MappingCouchbaseConverterTests { private static MappingCouchbaseConverter converter = new MappingCouchbaseConverter(); + private static MappingCouchbaseConverter customConverter = (new Config()).mappingCouchbaseConverter(); static { converter.afterPropertiesSet(); + customConverter.afterPropertiesSet(); } @Test @@ -112,6 +116,18 @@ public class MappingCouchbaseConverterTests { assertThat(converted.getId()).isEqualTo(BaseEntity.ID); } + @Test + void writesNumberCustom() { + CouchbaseDocument converted = new CouchbaseDocument(); + NumberEntity entity = new NumberEntity(42); + + customConverter.write(entity, converted); + Map result = converted.export(); + assertThat(result.get("t")).isEqualTo(entity.getClass().getName()); + assertThat(result.get("attr0")).isEqualTo(42L); + assertThat(converted.getId()).isEqualTo(BaseEntity.ID); + } + @Test void readsNumber() { CouchbaseDocument source = new CouchbaseDocument(); @@ -363,6 +379,40 @@ public class MappingCouchbaseConverterTests { assertThat(readConverted.listOfEmails.get(0).emailAddr).isEqualTo(listOfEmails.get(0).emailAddr); } + @Test + void writesAndReadsValueClassCustomType() { + CouchbaseDocument converted = new CouchbaseDocument(); + + final String email = "foo@bar.com"; + final Emailx addy = new Emailx(email); + List listOfEmails = new ArrayList(); + listOfEmails.add(addy); + + ValueEntityx entity = new ValueEntityx(addy, listOfEmails); + customConverter.write(entity, converted); + Map result = converted.export(); + + assertThat(result.get("t")).isEqualTo(ValueEntityx.class.getAnnotation(TypeAlias.class).value()); + assertThat(result.get("email")).isEqualTo(new HashMap() { + { + put("emailAddr", email); + } + }); + + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", ValueEntity.class.getName()); + CouchbaseDocument emailDoc = new CouchbaseDocument(); + emailDoc.put("emailAddr", "foo@bar.com"); + source.put("email", emailDoc); + CouchbaseList listOfEmailsDoc = new CouchbaseList(); + listOfEmailsDoc.put(emailDoc); + source.put("listOfEmails", listOfEmailsDoc); + + ValueEntity readConverted = converter.read(ValueEntity.class, source); + assertThat(readConverted.email.emailAddr).isEqualTo(addy.emailAddr); + assertThat(readConverted.listOfEmails.get(0).emailAddr).isEqualTo(listOfEmails.get(0).emailAddr); + } + @Test void writesAndReadsCustomConvertedClass() { List converters = new ArrayList<>(); @@ -693,6 +743,26 @@ public class MappingCouchbaseConverterTests { } } + @TypeAlias("x") + static class ValueEntityx extends BaseEntity { + private Emailx email; + private List listOfEmails; + + public ValueEntityx(Emailx email, List listOfEmails) { + this.email = email; + this.listOfEmails = listOfEmails; + } + } + + // @TypeAlias("x") + static class Emailx { + private String emailAddr; + + public Emailx(String emailAddr) { + this.emailAddr = emailAddr; + } + } + static class CustomObject { private BigDecimal weight; diff --git a/src/test/java/org/springframework/data/couchbase/domain/Config.java b/src/test/java/org/springframework/data/couchbase/domain/Config.java index 4661b92b..2864eff9 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Config.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Config.java @@ -16,6 +16,10 @@ package org.springframework.data.couchbase.domain; +import org.springframework.data.couchbase.config.BeanNames; +import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.auditing.DateTimeProvider; @@ -113,4 +117,38 @@ public class Config extends AbstractCouchbaseConfiguration { public DateTimeProvider testDateTimeProvider() { return new AuditingDateTimeProvider(); } + + // convenience constructor for tests + public MappingCouchbaseConverter mappingCouchbaseConverter() { + MappingCouchbaseConverter converter = null; + try { + // MappingCouchbaseConverter relies on a SimpleInformationMapper + // that has an getAliasFor(info) that just returns getType().getName(). + // Our CustomMappingCouchbaseConverter uses a TypeBasedCouchbaseTypeMapper that will + // use the DocumentType annotation + converter = new CustomMappingCouchbaseConverter(couchbaseMappingContext(customConversions()), typeKey()); + } catch (Exception e) { + throw new RuntimeException(e); + } + return converter; + } + + @Override + @Bean(name = "couchbaseMappingConverter") + public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingContext couchbaseMappingContext, + CouchbaseCustomConversions couchbaseCustomConversions) { + // MappingCouchbaseConverter relies on a SimpleInformationMapper + // that has an getAliasFor(info) that just returns getType().getName(). + // Our CustomMappingCouchbaseConverter uses a TypeBasedCouchbaseTypeMapper that will + // use the DocumentType annotation + MappingCouchbaseConverter converter = new CustomMappingCouchbaseConverter(couchbaseMappingContext, typeKey()); + converter.setCustomConversions(couchbaseCustomConversions); + return converter; + } + + @Override + public String typeKey() { + return "t"; // this will override '_class', is passed in to new CustomMappingCouchbaseConverter + } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/CustomMappingCouchbaseConverter.java b/src/test/java/org/springframework/data/couchbase/domain/CustomMappingCouchbaseConverter.java new file mode 100644 index 00000000..e17e83c2 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/CustomMappingCouchbaseConverter.java @@ -0,0 +1,23 @@ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.mapping.context.MappingContext; + +public class CustomMappingCouchbaseConverter extends MappingCouchbaseConverter { + + /** + * this constructer creates a TypeBasedCouchbaseTypeMapper with the specified typeKey + * while MappingCouchbaseConverter uses a DefaultCouchbaseTypeMapper + * typeMapper = new DefaultCouchbaseTypeMapper(typeKey != null ? typeKey : TYPEKEY_DEFAULT); + * @param mappingContext + * @param typeKey - the typeKey to be used (normally "_class") + */ + public CustomMappingCouchbaseConverter(final MappingContext, CouchbasePersistentProperty> mappingContext, final String typeKey) { + super(mappingContext, typeKey); + this.typeMapper = new TypeBasedCouchbaseTypeMapper(typeKey); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index eede7eb5..ce79036d 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -18,14 +18,11 @@ package org.springframework.data.couchbase.domain; import java.util.Optional; import java.util.UUID; -import org.springframework.data.annotation.CreatedBy; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedBy; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.annotation.Version; +import org.springframework.data.annotation.*; import org.springframework.data.couchbase.core.mapping.Document; @Document +@TypeAlias("p") // this will result in p : or p : "t" using the CustomMappingCouchbaseConverter public class Person extends AbstractEntity { Optional firstname; Optional lastname; @@ -40,11 +37,13 @@ public class Person extends AbstractEntity { private long lastModification; @CreatedDate - private long creationDate; // =System.currentTimeMillis(); + private long creationDate; @Version private long version; + private Object middlename; + public Person() { } @@ -132,4 +131,7 @@ public class Person extends AbstractEntity { return ""; } + public void setMiddlename(String middlename) { + this.middlename = middlename; + } } diff --git a/src/test/java/org/springframework/data/couchbase/domain/TypeAwareTypeInformationMapper.java b/src/test/java/org/springframework/data/couchbase/domain/TypeAwareTypeInformationMapper.java new file mode 100644 index 00000000..fd6d5b02 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/TypeAwareTypeInformationMapper.java @@ -0,0 +1,20 @@ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.convert.SimpleTypeInformationMapper; +import org.springframework.data.mapping.Alias; +import org.springframework.data.util.TypeInformation; + +public class TypeAwareTypeInformationMapper extends SimpleTypeInformationMapper { + + @Override + public Alias createAliasFor(TypeInformation type) { + TypeAlias[] typeAlias = type.getType().getAnnotationsByType(TypeAlias.class); + + if (typeAlias.length == 1) { + return Alias.of(typeAlias[0].value()); + } + + return super.createAliasFor(type); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/TypeBasedCouchbaseTypeMapper.java b/src/test/java/org/springframework/data/couchbase/domain/TypeBasedCouchbaseTypeMapper.java new file mode 100644 index 00000000..2448c730 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/TypeBasedCouchbaseTypeMapper.java @@ -0,0 +1,31 @@ +package org.springframework.data.couchbase.domain; + +import org.springframework.data.convert.DefaultTypeMapper; +import org.springframework.data.couchbase.core.convert.CouchbaseTypeMapper; +import org.springframework.data.couchbase.core.convert.DefaultCouchbaseTypeMapper; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.mapping.Alias; +import org.springframework.data.util.TypeInformation; + +import java.util.Collections; + +public class TypeBasedCouchbaseTypeMapper extends DefaultTypeMapper implements CouchbaseTypeMapper { + + private final String typeKey; + + public TypeBasedCouchbaseTypeMapper(final String typeKey) { + super(new DefaultCouchbaseTypeMapper.CouchbaseDocumentTypeAliasAccessor(typeKey), + Collections.singletonList(new TypeAwareTypeInformationMapper())); + this.typeKey = typeKey; + } + + @Override + public String getTypeKey() { + return typeKey; + } + + @Override + public Alias getTypeAlias(TypeInformation info) { + return getAliasFor(info); + } +}