DATACOUCH-55 - Support custom converters.

This changeset adds the possibility to implement custom converters for entities and fields.
This commit is contained in:
Michael Nitschinger
2014-03-07 17:35:36 +01:00
parent 38b1a1bd1e
commit 332ce8c560
10 changed files with 1004 additions and 132 deletions

View File

@@ -25,6 +25,7 @@ import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.couchbase.core.CouchbaseFactoryBean;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.convert.CustomConversions;
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;
@@ -94,11 +95,11 @@ public abstract class AbstractCouchbaseConfiguration {
/**
* Prepare the logging property before initializing couchbase.
*
* @param logger
* @param logger the logger path to use.
*/
private void setLoggerProperty(String logger) {
private static void setLoggerProperty(final String logger) {
Properties systemProperties = System.getProperties();
systemProperties.put("net.spy.log.LoggerImpl", logger);
systemProperties.setProperty("net.spy.log.LoggerImpl", logger);
System.setProperties(systemProperties);
}
@@ -119,7 +120,9 @@ public abstract class AbstractCouchbaseConfiguration {
*/
@Bean
public MappingCouchbaseConverter mappingCouchbaseConverter() throws Exception {
return new MappingCouchbaseConverter(couchbaseMappingContext());
MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext());
converter.setCustomConversions(customConversions());
return converter;
}
/**
@@ -143,9 +146,22 @@ public abstract class AbstractCouchbaseConfiguration {
public CouchbaseMappingContext couchbaseMappingContext() throws Exception {
CouchbaseMappingContext mappingContext = new CouchbaseMappingContext();
mappingContext.setInitialEntitySet(getInitialEntitySet());
mappingContext.setSimpleTypeHolder(customConversions().getSimpleTypeHolder());
return mappingContext;
}
/**
* Register custom Converters in a {@link CustomConversions} object if required. These
* {@link CustomConversions} will be registered with the {@link #mappingCouchbaseConverter()} and
* {@link #couchbaseMappingContext()}. Returns an empty {@link CustomConversions} instance by default.
*
* @return must not be {@literal null}.
*/
@Bean
public CustomConversions customConversions() {
return new CustomConversions(Collections.emptyList());
}
/**
* Scans the mapping base package for classes annotated with {@link Document}.
*
@@ -188,7 +204,7 @@ public abstract class AbstractCouchbaseConfiguration {
* @param hosts the list of hosts to convert.
* @return the converted URIs.
*/
private List<URI> bootstrapUris(List<String> hosts) throws URISyntaxException {
private static List<URI> bootstrapUris(List<String> hosts) throws URISyntaxException {
List<URI> uris = new ArrayList<URI>();
for (String host : hosts) {
uris.add(new URI("http://" + host + ":8091/pools"));

View File

@@ -65,7 +65,7 @@ public class CouchbaseTemplate implements CouchbaseOperations {
private final CouchbaseClient client;
protected final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
private final CouchbaseExceptionTranslator exceptionTranslator = new CouchbaseExceptionTranslator();
private final TranslationService<String> translationService;
private final TranslationService translationService;
private CouchbaseConverter couchbaseConverter;
private WriteResultChecking writeResultChecking = DEFAULT_WRITE_RESULT_CHECKING;

View File

@@ -48,7 +48,7 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
*
* @param conversionService the conversion service to use.
*/
public AbstractCouchbaseConverter(final GenericConversionService conversionService) {
protected AbstractCouchbaseConverter(final GenericConversionService conversionService) {
this.conversionService = conversionService;
}
@@ -57,15 +57,35 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
*
* @return the conversion service.
*/
@Override
public ConversionService getConversionService() {
return conversionService;
}
/**
* Set the custom conversions.
*
* @param conversions the conversions.
*/
public void setCustomConversions(final CustomConversions conversions) {
this.conversions = conversions;
}
/**
* Set the entity instantiators.
*
* @param instantiators the instantiators.
*/
public void setInstantiators(final EntityInstantiators instantiators) {
this.instantiators = instantiators;
}
/**
* Do nothing after the properties set on the bean.
*/
@Override
public void afterPropertiesSet() {
conversions.registerConvertersIn(conversionService);
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2014 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.convert;
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
import org.springframework.data.couchbase.core.mapping.CouchbaseSimpleTypes;
import org.springframework.util.Assert;
/**
* Conversion registration information.
*
* @author Oliver Gierke
* @author Michael Nitschinger
*/
class ConverterRegistration {
private final ConvertiblePair convertiblePair;
private final boolean reading;
private final boolean writing;
/**
* Creates a new {@link ConverterRegistration}.
*
* @param convertiblePair must not be {@literal null}.
* @param isReading whether to force to consider the converter for reading.
* @param isWriting whether to force to consider the converter for reading.
*/
public ConverterRegistration(ConvertiblePair convertiblePair, boolean isReading, boolean isWriting) {
Assert.notNull(convertiblePair);
this.convertiblePair = convertiblePair;
reading = isReading;
writing = isWriting;
}
/**
* Creates a new {@link ConverterRegistration} from the given source and target type and read/write flags.
*
* @param source the source type to be converted from, must not be {@literal null}.
* @param target the target type to be converted to, must not be {@literal null}.
* @param isReading whether to force to consider the converter for reading.
* @param isWriting whether to force to consider the converter for writing.
*/
public ConverterRegistration(Class<?> source, Class<?> target, boolean isReading, boolean isWriting) {
this(new ConvertiblePair(source, target), isReading, isWriting);
}
/**
* Returns whether the converter shall be used for writing.
*
* @return
*/
public boolean isWriting() {
return writing == true || (!reading && isSimpleTargetType());
}
/**
* Returns whether the converter shall be used for reading.
*
* @return
*/
public boolean isReading() {
return reading == true || (!writing && isSimpleSourceType());
}
/**
* Returns the actual conversion pair.
*
* @return
*/
public ConvertiblePair getConvertiblePair() {
return convertiblePair;
}
/**
* Returns whether the source type is a Mongo simple one.
*
* @return
*/
public boolean isSimpleSourceType() {
return isCouchbaseBasicType(convertiblePair.getSourceType());
}
/**
* Returns whether the target type is a Mongo simple one.
*
* @return
*/
public boolean isSimpleTargetType() {
return isCouchbaseBasicType(convertiblePair.getTargetType());
}
/**
* Returns whether the given type is a type that Mongo can handle basically.
*
* @param type
* @return
*/
private static boolean isCouchbaseBasicType(Class<?> type) {
return CouchbaseSimpleTypes.HOLDER.isSimpleType(type);
}
}

View File

@@ -16,11 +16,22 @@
package org.springframework.data.couchbase.core.convert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.JodaTimeConverters;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Value object to capture custom conversion.
@@ -29,14 +40,26 @@ import java.util.List;
* inspection nor nested conversion.</p>
*
* @author Michael Nitschinger
* @author Oliver Gierke
*/
public class CustomConversions {
private static final Logger LOG = LoggerFactory.getLogger(CustomConversions.class);
private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a Couchbase supported type! You might wanna check you annotation setup at the converter implementation.";
private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a Couchbase supported type! You might wanna check you annotation setup at the converter implementation.";
/**
* Contains the simple type holder.
*/
private final SimpleTypeHolder simpleTypeHolder;
private final List<Object> converters;
private final Set<GenericConverter.ConvertiblePair> readingPairs;
private final Set<GenericConverter.ConvertiblePair> writingPairs;
private final Set<Class<?>> customSimpleTypes;
private final ConcurrentMap<GenericConverter.ConvertiblePair, CacheValue> customReadTargetTypes;
/**
* Create a new instance with no converters.
*/
@@ -51,6 +74,19 @@ public class CustomConversions {
public CustomConversions(final List<?> converters) {
Assert.notNull(converters);
readingPairs = new LinkedHashSet<GenericConverter.ConvertiblePair>();
writingPairs = new LinkedHashSet<GenericConverter.ConvertiblePair>();
customSimpleTypes = new HashSet<Class<?>>();
customReadTargetTypes = new ConcurrentHashMap<GenericConverter.ConvertiblePair, CacheValue>();
this.converters = new ArrayList<Object>();
this.converters.addAll(converters);
this.converters.addAll(JodaTimeConverters.getConvertersToRegister());
for (Object converter : this.converters) {
registerConversion(converter);
}
simpleTypeHolder = new SimpleTypeHolder();
}
@@ -64,4 +100,234 @@ public class CustomConversions {
return simpleTypeHolder.isSimpleType(type);
}
/**
* Returns the simple type holder.
*
* @return the simple type holder.
*/
public SimpleTypeHolder getSimpleTypeHolder() {
return simpleTypeHolder;
}
/**
* Populates the given {@link GenericConversionService} with the convertes registered.
*
* @param conversionService the service to register.
*/
public void registerConvertersIn(final GenericConversionService conversionService) {
for (Object converter : converters) {
boolean added = false;
if (converter instanceof Converter) {
conversionService.addConverter((Converter<?, ?>) converter);
added = true;
}
if (converter instanceof ConverterFactory) {
conversionService.addConverterFactory((ConverterFactory<?, ?>) converter);
added = true;
}
if (converter instanceof GenericConverter) {
conversionService.addConverter((GenericConverter) converter);
added = true;
}
if (!added) {
throw new IllegalArgumentException("Given set contains element that is neither Converter nor ConverterFactory!");
}
}
}
/**
* Registers a conversion for the given converter. Inspects either generics or the convertible pairs returned
* by a {@link GenericConverter}.
*
* @param converter the converter to register.
*/
private void registerConversion(final Object converter) {
Class<?> type = converter.getClass();
boolean isWriting = type.isAnnotationPresent(WritingConverter.class);
boolean isReading = type.isAnnotationPresent(ReadingConverter.class);
if (converter instanceof GenericConverter) {
GenericConverter genericConverter = (GenericConverter) converter;
for (GenericConverter.ConvertiblePair pair : genericConverter.getConvertibleTypes()) {
register(new ConverterRegistration(pair, isReading, isWriting));
}
} else if (converter instanceof Converter) {
Class<?>[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), Converter.class);
register(new ConverterRegistration(arguments[0], arguments[1], isReading, isWriting));
} else {
throw new IllegalArgumentException("Unsupported Converter type!");
}
}
/**
* Registers the given {@link ConverterRegistration} as reading or writing pair depending on the type sides being basic
* Couchbase types.
*
* @param registration the registration.
*/
private void register(final ConverterRegistration registration) {
GenericConverter.ConvertiblePair pair = registration.getConvertiblePair();
if (registration.isReading()) {
readingPairs.add(pair);
if (LOG.isWarnEnabled() && !registration.isSimpleSourceType()) {
LOG.warn(String.format(READ_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType()));
}
}
if (registration.isWriting()) {
writingPairs.add(pair);
customSimpleTypes.add(pair.getSourceType());
if (LOG.isWarnEnabled() && !registration.isSimpleTargetType()) {
LOG.warn(String.format(WRITE_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType()));
}
}
}
/**
* Returns the target type to convert to in case we have a custom conversion registered to convert the given source
* type into a Couchbase native one.
*
* @param sourceType must not be {@literal null}
* @return
*/
public Class<?> getCustomWriteTarget(Class<?> sourceType) {
return getCustomWriteTarget(sourceType, null);
}
/**
* Returns the target type we can write an object of the given source type to. The returned type might be a subclass
* oth the given expected type though. If {@code expectedTargetType} is {@literal null} we will simply return the
* first target type matching or {@literal null} if no conversion can be found.
*
* @param sourceType must not be {@literal null}
* @param requestedTargetType
* @return
*/
public Class<?> getCustomWriteTarget(Class<?> sourceType, Class<?> requestedTargetType) {
Assert.notNull(sourceType);
return getCustomTarget(sourceType, requestedTargetType, writingPairs);
}
/**
* Returns whether we have a custom conversion registered to write into a Couchbase native type. The returned type might
* be a subclass of the given expected type though.
*
* @param sourceType must not be {@literal null}
* @return
*/
public boolean hasCustomWriteTarget(Class<?> sourceType) {
Assert.notNull(sourceType);
return hasCustomWriteTarget(sourceType, null);
}
/**
* Returns whether we have a custom conversion registered to write an object of the given source type into an object
* of the given Couchbase native target type.
*
* @param sourceType must not be {@literal null}.
* @param requestedTargetType
* @return
*/
public boolean hasCustomWriteTarget(Class<?> sourceType, Class<?> requestedTargetType) {
Assert.notNull(sourceType);
return getCustomWriteTarget(sourceType, requestedTargetType) != null;
}
/**
* Returns whether we have a custom conversion registered to read the given source into the given target type.
*
* @param sourceType must not be {@literal null}
* @param requestedTargetType must not be {@literal null}
* @return
*/
public boolean hasCustomReadTarget(Class<?> sourceType, Class<?> requestedTargetType) {
Assert.notNull(sourceType);
Assert.notNull(requestedTargetType);
return getCustomReadTarget(sourceType, requestedTargetType) != null;
}
/**
* Returns the actual target type for the given {@code sourceType} and {@code requestedTargetType}. Note that the
* returned {@link Class} could be an assignable type to the given {@code requestedTargetType}.
*
* @param sourceType must not be {@literal null}.
* @param requestedTargetType can be {@literal null}.
* @return
*/
private Class<?> getCustomReadTarget(Class<?> sourceType, Class<?> requestedTargetType) {
Assert.notNull(sourceType);
if (requestedTargetType == null) {
return null;
}
GenericConverter.ConvertiblePair lookupKey = new GenericConverter.ConvertiblePair(sourceType, requestedTargetType);
CacheValue readTargetTypeValue = customReadTargetTypes.get(lookupKey);
if (readTargetTypeValue != null) {
return readTargetTypeValue.getType();
}
readTargetTypeValue = CacheValue.of(getCustomTarget(sourceType, requestedTargetType, readingPairs));
CacheValue cacheValue = customReadTargetTypes.putIfAbsent(lookupKey, readTargetTypeValue);
return cacheValue != null ? cacheValue.getType() : readTargetTypeValue.getType();
}
/**
* Inspects the given {@link org.springframework.core.convert.converter.GenericConverter.ConvertiblePair} for ones
* that have a source compatible type as source. Additionally checks assignability of the target type if one is
* given.
*
* @param sourceType must not be {@literal null}.
* @param requestedTargetType can be {@literal null}.
* @param pairs must not be {@literal null}.
* @return
*/
private static Class<?> getCustomTarget(Class<?> sourceType, Class<?> requestedTargetType,
Iterable<GenericConverter.ConvertiblePair> pairs) {
Assert.notNull(sourceType);
Assert.notNull(pairs);
for (GenericConverter.ConvertiblePair typePair : pairs) {
if (typePair.getSourceType().isAssignableFrom(sourceType)) {
Class<?> targetType = typePair.getTargetType();
if (requestedTargetType == null || targetType.isAssignableFrom(requestedTargetType)) {
return targetType;
}
}
}
return null;
}
/**
* Wrapper to safely store {@literal null} values in the type cache.
*
* @author Patryk Wasik
* @author Oliver Gierke
* @author Thomas Darimont
*/
private static class CacheValue {
private static final CacheValue ABSENT = new CacheValue(null);
private final Class<?> type;
public CacheValue(Class<?> type) {
this.type = type;
}
public Class<?> getType() {
return type;
}
static CacheValue of(Class<?> type) {
return type == null ? ABSENT : new CacheValue(type);
}
}
}

View File

@@ -16,7 +16,6 @@
package org.springframework.data.couchbase.core.convert;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.CollectionFactory;
@@ -41,50 +40,90 @@ import org.springframework.util.CollectionUtils;
import java.util.*;
/**
* The Couchbase special {@link MappingCouchbaseConverter}.
* <p/>
* This converter is responsible for mapping (read and writing) value from and to target formats.
* A mapping converter for Couchbase.
*
* The converter is responsible for reading from and writing to entities and converting it into a
* consumable database represenation.
*
* @author Michael Nitschinger
*/
public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
implements ApplicationContextAware {
implements ApplicationContextAware {
/**
* The overall application context.
*/
protected ApplicationContext applicationContext;
protected final MappingContext<? extends CouchbasePersistentEntity<?>,
CouchbasePersistentProperty> mappingContext;
protected boolean useFieldAccessOnly = true;
protected CouchbaseTypeMapper typeMapper;
private SpELContext spELContext;
/**
* The generic mapping context.
*/
protected final MappingContext<? extends CouchbasePersistentEntity<?>,
CouchbasePersistentProperty> mappingContext;
/**
* Always use field access only.
*/
protected boolean useFieldAccessOnly = true;
/**
* The Couchbase specific type mapper in use.
*/
protected CouchbaseTypeMapper typeMapper;
/**
* Spring Expression Language context.
*/
private final SpELContext spELContext;
/**
* Create a new {@link MappingCouchbaseConverter}.
*
* @param mappingContext the mapping context to use.
*/
@SuppressWarnings("deprecation")
public MappingCouchbaseConverter(MappingContext<? extends CouchbasePersistentEntity<?>,
CouchbasePersistentProperty> mappingContext) {
public MappingCouchbaseConverter(final MappingContext<? extends CouchbasePersistentEntity<?>,
CouchbasePersistentProperty> mappingContext) {
super(ConversionServiceFactory.createDefaultConversionService());
this.mappingContext = mappingContext;
typeMapper = new DefaultCouchbaseTypeMapper(DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY);
spELContext = new SpELContext(CouchbaseDocumentPropertyAccessor.INSTANCE);
}
@Override
public MappingContext<? extends CouchbasePersistentEntity<?>,
CouchbasePersistentProperty> getMappingContext() {
public MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> getMappingContext() {
return mappingContext;
}
@Override
public <R> R read(Class<R> clazz, CouchbaseDocument doc) {
return read(ClassTypeInformation.from(clazz), doc, null);
public <R> R read(final Class<R> clazz, final CouchbaseDocument source) {
return read(ClassTypeInformation.from(clazz), source, null);
}
protected <R> R read(TypeInformation<R> type, CouchbaseDocument doc) {
return read(type, doc, null);
/**
* Read an incoming {@link CouchbaseDocument} into the target entity.
*
* @param type the type information of the target entity.
* @param source the document to convert.
* @param <R> the entity type.
* @return the converted entity.
*/
protected <R> R read(final TypeInformation<R> type, final CouchbaseDocument source) {
return read(type, source, null);
}
protected <R> R read(TypeInformation<R> type, final CouchbaseDocument source, Object parent) {
/**
* Read an incoming {@link CouchbaseDocument} into the target entity.
*
* @param type the type information of the target entity.
* @param source the document to convert.
* @param parent an optional parent object.
* @param <R> the entity type.
* @return the converted entity.
*/
@SuppressWarnings("unchecked")
protected <R> R read(final TypeInformation<R> type, final CouchbaseDocument source, final Object parent) {
if (source == null) {
return null;
}
@@ -92,50 +131,60 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
TypeInformation<? extends R> typeToUse = typeMapper.readType(source, type);
Class<? extends R> rawType = typeToUse.getType();
if (conversions.hasCustomReadTarget(source.getClass(), rawType)) {
return conversionService.convert(source, rawType);
}
if (typeToUse.isMap()) {
return (R) readMap(typeToUse, source, parent);
}
CouchbasePersistentEntity<R> persistentEntity = (CouchbasePersistentEntity<R>)
mappingContext.getPersistentEntity(typeToUse);
if (persistentEntity == null) {
CouchbasePersistentEntity<R> entity = (CouchbasePersistentEntity<R>) mappingContext.getPersistentEntity(typeToUse);
if (entity == null) {
throw new MappingException("No mapping metadata found for " + rawType.getName());
}
return read(persistentEntity, source, parent);
return read(entity, source, parent);
}
/**
* Read an incoming {@link CouchbaseDocument} into the target entity.
*
* @param entity the target entity.
* @param source the document to convert.
* @param parent an optional parent object.
* @param <R> the entity type.
* @return the converted entity.
*/
protected <R> R read(final CouchbasePersistentEntity<R> entity, final CouchbaseDocument source, final Object parent) {
final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext);
ParameterValueProvider<CouchbasePersistentProperty> provider = getParameterProvider(entity, source, evaluator, parent);
ParameterValueProvider<CouchbasePersistentProperty> provider =
getParameterProvider(entity, source, evaluator, parent);
EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity);
R instance = instantiator.createInstance(entity, provider);
R instance = instantiator.createInstance(entity, provider);
final BeanWrapper<CouchbasePersistentEntity<R>, R> wrapper = BeanWrapper.create(instance, conversionService);
final R result = wrapper.getBean();
entity.doWithProperties(new PropertyHandler<CouchbasePersistentProperty>() {
@Override
public void doWithPersistentProperty(final CouchbasePersistentProperty prop) {
if (!doesPropertyExistInSource(prop) || entity.isConstructorArgument(prop)) {
return;
}
Object obj = prop.isIdProperty() ? source.getId() : getValueInternal(prop, source, evaluator, result);
Object obj = prop.isIdProperty() ? source.getId() : getValueInternal(prop, source, result);
wrapper.setProperty(prop, obj, useFieldAccessOnly);
}
private boolean doesPropertyExistInSource(CouchbasePersistentProperty property) {
private boolean doesPropertyExistInSource(final CouchbasePersistentProperty property) {
return property.isIdProperty() || source.containsKey(property.getFieldName());
}
});
entity.doWithAssociations(new AssociationHandler<CouchbasePersistentProperty>() {
@Override
public void doWithAssociation(final Association<CouchbasePersistentProperty> association) {
CouchbasePersistentProperty inverseProp = association.getInverse();
Object obj = getValueInternal(inverseProp, source, evaluator, result);
Object obj = getValueInternal(inverseProp, source, result);
wrapper.setProperty(inverseProp, obj);
}
});
@@ -143,29 +192,55 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
return result;
}
protected Object getValueInternal(CouchbasePersistentProperty prop, CouchbaseDocument source, SpELExpressionEvaluator eval,
Object parent) {
CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, spELContext, parent);
return provider.getPropertyValue(prop);
/**
* Loads the property value through the value provider.
*
* @param property the source property.
* @param source the source document.
* @param parent the optional parent.
* @return the actual property value.
*/
protected Object getValueInternal(final CouchbasePersistentProperty property, final CouchbaseDocument source,
final Object parent) {
return new CouchbasePropertyValueProvider(source, spELContext, parent).getPropertyValue(property);
}
private ParameterValueProvider<CouchbasePersistentProperty> getParameterProvider(CouchbasePersistentEntity<?> entity,
CouchbaseDocument source, DefaultSpELExpressionEvaluator evaluator, Object parent) {
/**
* Creates a new parameter provider.
*
* @param entity the persistent entity.
* @param source the source document.
* @param evaluator the SPEL expression evaluator.
* @param parent the optional parent.
* @return a new parameter value provider.
*/
private ParameterValueProvider<CouchbasePersistentProperty> getParameterProvider(
final CouchbasePersistentEntity<?> entity, final CouchbaseDocument source,
final DefaultSpELExpressionEvaluator evaluator, final Object parent) {
CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, evaluator, parent);
PersistentEntityParameterValueProvider<CouchbasePersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<CouchbasePersistentProperty>(
entity, provider, parent);
PersistentEntityParameterValueProvider<CouchbasePersistentProperty> parameterProvider =
new PersistentEntityParameterValueProvider<CouchbasePersistentProperty>(entity, provider, parent);
return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider,
parent);
parent);
}
protected Map<Object, Object> readMap(TypeInformation<?> type, CouchbaseDocument doc, Object parent) {
Assert.notNull(doc);
/**
* Recursively parses the a map from the source document.
*
* @param type the type information for the document.
* @param source the source document.
* @param parent the optional parent.
* @return the recursively parsed map.
*/
@SuppressWarnings("unchecked")
protected Map<Object, Object> readMap(final TypeInformation<?> type, final CouchbaseDocument source,
final Object parent) {
Assert.notNull(source);
Class<?> mapType = typeMapper.readType(doc, type).getType();
Map<Object, Object> map = CollectionFactory.createMap(mapType, doc.export().keySet().size());
Map<String, Object> sourceMap = doc.getPayload();
Class<?> mapType = typeMapper.readType(source, type).getType();
Map<Object, Object> map = CollectionFactory.createMap(mapType, source.export().keySet().size());
Map<String, Object> sourceMap = source.getPayload();
for (Map.Entry<String, Object> entry : sourceMap.entrySet()) {
Object key = entry.getKey();
@@ -178,7 +253,6 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
}
TypeInformation<?> valueType = type.getMapValueType();
if (value instanceof CouchbaseDocument) {
map.put(key, read(valueType, (CouchbaseDocument) value, parent));
} else if (value instanceof CouchbaseList) {
@@ -192,12 +266,23 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
return map;
}
private Object getPotentiallyConvertedSimpleRead(Object value, Class<?> target) {
/**
* Potentially convert simple values like ENUMs.
*
* @param value the value to convert.
* @param target the target object.
* @return the potentially converted object.
*/
@SuppressWarnings("unchecked")
private Object getPotentiallyConvertedSimpleRead(final Object value, final Class<?> target) {
if (value == null || target == null) {
return value;
}
if (conversions.hasCustomReadTarget(value.getClass(), target)) {
return conversionService.convert(value, target);
}
if (Enum.class.isAssignableFrom(target)) {
return Enum.valueOf((Class<Enum>) target, value.toString());
}
@@ -219,20 +304,38 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
return;
}
boolean isCustom = conversions.getCustomWriteTarget(source.getClass(), CouchbaseDocument.class) != null;
TypeInformation<?> type = ClassTypeInformation.from(source.getClass());
typeMapper.writeType(type, target);
writeInternal(source, target, type);
if (!isCustom) {
typeMapper.writeType(type, target);
}
writeInternal(source, target, type);
if (target.getId() == null) {
throw new MappingException("An ID property is needed, but not found on this entity.");
}
}
protected void writeInternal(final Object source, final CouchbaseDocument target, final TypeInformation<?> typeHint) {
/**
* Convert a source object into a {@link CouchbaseDocument} target.
*
* @param source the source object.
* @param target the target document.
* @param typeHint the type information for the source.
*/
@SuppressWarnings("unchecked")
protected void writeInternal(final Object source, CouchbaseDocument target, final TypeInformation<?> typeHint) {
if (source == null) {
return;
}
Class<?> customTarget = conversions.getCustomWriteTarget(source.getClass(), CouchbaseDocument.class);
if (customTarget != null) {
copyCouchbaseDocument(conversionService.convert(source, CouchbaseDocument.class), target);
return;
}
if (Map.class.isAssignableFrom(source.getClass())) {
writeMapInternal((Map<Object, Object>) source, target, ClassTypeInformation.MAP);
return;
@@ -247,7 +350,29 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
addCustomTypeKeyIfNecessary(typeHint, source, target);
}
protected void writeInternal(final Object source, final CouchbaseDocument target, final CouchbasePersistentEntity<?> entity) {
/**
* Helper method to copy the internals from a source document into a target document.
*
* @param source the source document.
* @param target the target document.
*/
protected void copyCouchbaseDocument(final CouchbaseDocument source, final CouchbaseDocument target) {
for (Map.Entry<String, Object> entry : source.export().entrySet()) {
target.put(entry.getKey(), entry.getValue());
}
target.setId(source.getId());
target.setExpiration(source.getExpiration());
}
/**
* Internal helper method to write the source object into the target document.
*
* @param source the source object.
* @param target the target document.
* @param entity the persistent entity to convert from.
*/
protected void writeInternal(final Object source, final CouchbaseDocument target,
final CouchbasePersistentEntity<?> entity) {
if (source == null) {
return;
}
@@ -256,7 +381,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
throw new MappingException("No mapping metadata found for entity of type " + source.getClass().getName());
}
final BeanWrapper<CouchbasePersistentEntity<Object>, Object> wrapper = BeanWrapper.create(source, conversionService);
final BeanWrapper<CouchbasePersistentEntity<Object>, Object> wrapper = BeanWrapper.create(source,
conversionService);
final CouchbasePersistentProperty idProperty = entity.getIdProperty();
final CouchbasePersistentProperty versionProperty = entity.getVersionProperty();
@@ -267,11 +393,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
target.setExpiration(entity.getExpiry());
entity.doWithProperties(new PropertyHandler<CouchbasePersistentProperty>() {
@Override
public void doWithPersistentProperty(final CouchbasePersistentProperty prop) {
if (prop.equals(idProperty)) {
return;
}
if (versionProperty != null && prop.equals(versionProperty)) {
if (prop.equals(idProperty) || (versionProperty != null && prop.equals(versionProperty))) {
return;
}
@@ -300,7 +424,16 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
}
private void writePropertyInternal(final Object source, final CouchbaseDocument target, final CouchbasePersistentProperty prop) {
/**
* Helper method to write a property into the target document.
*
* @param source the source object.
* @param target the target document.
* @param prop the property information.
*/
@SuppressWarnings("unchecked")
private void writePropertyInternal(final Object source, final CouchbaseDocument target,
final CouchbasePersistentProperty prop) {
if (source == null) {
return;
}
@@ -321,23 +454,45 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
return;
}
Class<?> basicTargetType = conversions.getCustomWriteTarget(source.getClass(), null);
if (basicTargetType != null) {
target.put(name, conversionService.convert(source, basicTargetType));
return;
}
CouchbaseDocument propertyDoc = new CouchbaseDocument();
addCustomTypeKeyIfNecessary(type, source, propertyDoc);
CouchbasePersistentEntity<?> entity = isSubtype(prop.getType(), source.getClass()) ? mappingContext
.getPersistentEntity(source.getClass()) : mappingContext.getPersistentEntity(type);
.getPersistentEntity(source.getClass()) : mappingContext.getPersistentEntity(type);
writeInternal(source, propertyDoc, entity);
target.put(name, propertyDoc);
}
private CouchbaseDocument createMap(Map<Object, Object> map, CouchbasePersistentProperty prop) {
/**
* Wrapper method to create the underlying map.
*
* @param map the source map.
* @param prop the persistent property.
* @return the written couchbase document.
*/
private CouchbaseDocument createMap(final Map<Object, Object> map, final CouchbasePersistentProperty prop) {
Assert.notNull(map, "Given map must not be null!");
Assert.notNull(prop, "PersistentProperty must not be null!");
return writeMapInternal(map, new CouchbaseDocument(), prop.getTypeInformation());
}
private CouchbaseDocument writeMapInternal(Map<Object, Object> source, CouchbaseDocument target, TypeInformation<?> type) {
/**
* Helper method to write the map into the couchbase document.
*
* @param source the source object.
* @param target the target document.
* @param type the type information for the document.
* @return the written couchbase document.
*/
private CouchbaseDocument writeMapInternal(final Map<Object, Object> source, final CouchbaseDocument target,
final TypeInformation<?> type) {
for (Map.Entry<Object, Object> entry : source.entrySet()) {
Object key = entry.getKey();
Object val = entry.getValue();
@@ -363,11 +518,27 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
return target;
}
private CouchbaseList createCollection(Collection<?> collection, CouchbasePersistentProperty prop) {
/**
* Helper method to create the underlying collection/list.
*
* @param collection the collection to write.
* @param prop the property information.
* @return the created couchbase list.
*/
private CouchbaseList createCollection(final Collection<?> collection, final CouchbasePersistentProperty prop) {
return writeCollectionInternal(collection, new CouchbaseList(), prop.getTypeInformation());
}
private CouchbaseList writeCollectionInternal(Collection<?> source, CouchbaseList target, TypeInformation<?> type) {
/**
* Helper method to write the internal collection.
*
* @param source the source object.
* @param target the target document.
* @param type the type information for the document.
* @return the created couchbase list.
*/
private CouchbaseList writeCollectionInternal(final Collection<?> source, final CouchbaseList target,
final TypeInformation<?> type) {
TypeInformation<?> componentType = type == null ? null : type.getComponentType();
for (Object element : source) {
@@ -388,6 +559,15 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
return target;
}
/**
* Read a collection from the source object.
*
* @param targetType the target type.
* @param source the list as source.
* @param parent the optional parent.
* @return the instantiated collection.
*/
@SuppressWarnings("unchecked")
private Object readCollection(final TypeInformation<?> targetType, final CouchbaseList source, final Object parent) {
Assert.notNull(targetType);
@@ -418,9 +598,13 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
return getPotentiallyConvertedSimpleRead(items, targetType.getType());
}
/**
* Returns a collection from the given source object.
*
* @param source the source object.
* @return the target collection.
*/
private static Collection<?> asCollection(final Object source) {
if (source instanceof Collection) {
return (Collection<?>) source;
}
@@ -428,14 +612,48 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
}
private boolean isSubtype(final Class<?> left, final Class<?> right) {
/**
* Check if one class is a subtype of the other.
*
* @param left the first class.
* @param right the second class.
* @return true if it is a subtype, false otherwise.
*/
private static boolean isSubtype(final Class<?> left, final Class<?> right) {
return left.isAssignableFrom(right) && !left.equals(right);
}
private void writeSimpleInternal(Object source, CouchbaseDocument target, String key) {
target.put(key, source);
/**
* Write the given source into the couchbase document target.
*
* @param source the source object.
* @param target the target document.
* @param key the key of the object.
*/
private void writeSimpleInternal(final Object source, final CouchbaseDocument target, final String key) {
target.put(key, getPotentiallyConvertedSimpleWrite(source));
}
private Object getPotentiallyConvertedSimpleWrite(final Object value) {
if (value == null) {
return null;
}
Class<?> customTarget = conversions.getCustomWriteTarget(value.getClass(), null);
if (customTarget != null) {
return conversionService.convert(value, customTarget);
} else {
return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
}
}
/**
* Add a custom type key if needed.
*
* @param type the type information.
* @param source th the source object.
* @param target the target document.
*/
protected void addCustomTypeKeyIfNecessary(TypeInformation<?> type, Object source, CouchbaseDocument target) {
TypeInformation<?> actualType = type != null ? type.getActualType() : type;
Class<?> reference = actualType == null ? Object.class : actualType.getType();
@@ -447,52 +665,26 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
private class CouchbasePropertyValueProvider implements PropertyValueProvider<CouchbasePersistentProperty> {
private final CouchbaseDocument source;
private final SpELExpressionEvaluator evaluator;
private final Object parent;
public CouchbasePropertyValueProvider(CouchbaseDocument source, SpELContext factory, Object parent) {
this(source, new DefaultSpELExpressionEvaluator(source, factory), parent);
}
public CouchbasePropertyValueProvider(CouchbaseDocument source, DefaultSpELExpressionEvaluator evaluator, Object parent) {
Assert.notNull(source);
Assert.notNull(evaluator);
this.source = source;
this.evaluator = evaluator;
this.parent = parent;
}
public <R> R getPropertyValue(final CouchbasePersistentProperty property) {
String expression = property.getSpelExpression();
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName());
if (property.isIdProperty()) {
return (R) source.getId();
}
if (value == null) {
return null;
}
return readValue(value, property.getTypeInformation(), parent);
}
}
/**
* Helper method to read the value based on the value type.
*
* @param value the value to convert.
* @param type the type information.
* @param parent the optional parent.
* @param <R> the target type.
* @return the converted object.
*/
@SuppressWarnings("unchecked")
private <R> R readValue(Object value, TypeInformation<?> type, Object parent) {
Class<?> rawType = type.getType();
if (value instanceof CouchbaseDocument) {
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
return (R) conversionService.convert(value, rawType);
} else if (value instanceof CouchbaseDocument) {
return (R) read(type, (CouchbaseDocument) value, parent);
} else if (value instanceof CouchbaseList) {
return (R) readCollection(type, (CouchbaseList) value, parent);
@@ -501,20 +693,76 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
}
}
/**
* A property value provider for Couchbase documents.
*/
private class CouchbasePropertyValueProvider implements PropertyValueProvider<CouchbasePersistentProperty> {
/**
* The source document.
*/
private final CouchbaseDocument source;
/**
* The expression evaluator.
*/
private final SpELExpressionEvaluator evaluator;
/**
* The optional parent object.
*/
private final Object parent;
public CouchbasePropertyValueProvider(final CouchbaseDocument source, final SpELContext factory,
final Object parent) {
this(source, new DefaultSpELExpressionEvaluator(source, factory), parent);
}
public CouchbasePropertyValueProvider(final CouchbaseDocument source,
final DefaultSpELExpressionEvaluator evaluator, final Object parent) {
Assert.notNull(source);
Assert.notNull(evaluator);
this.source = source;
this.evaluator = evaluator;
this.parent = parent;
}
@Override
@SuppressWarnings("unchecked")
public <R> R getPropertyValue(final CouchbasePersistentProperty property) {
String expression = property.getSpelExpression();
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName());
if (property.isIdProperty()) {
return (R) source.getId();
}
if (value == null) {
return null;
}
return readValue(value, property.getTypeInformation(), parent);
}
}
/**
* A expression parameter value provider.
*/
private class ConverterAwareSpELExpressionParameterValueProvider extends
SpELExpressionParameterValueProvider<CouchbasePersistentProperty> {
SpELExpressionParameterValueProvider<CouchbasePersistentProperty> {
private final Object parent;
public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator,
ConversionService conversionService, ParameterValueProvider<CouchbasePersistentProperty> delegate, Object parent) {
public ConverterAwareSpELExpressionParameterValueProvider(final SpELExpressionEvaluator evaluator,
final ConversionService conversionService, final ParameterValueProvider<CouchbasePersistentProperty> delegate,
final Object parent) {
super(evaluator, conversionService, delegate);
this.parent = parent;
}
@Override
protected <T> T potentiallyConvertSpelValue(Object object, Parameter<T, CouchbasePersistentProperty> parameter) {
protected <T> T potentiallyConvertSpelValue(final Object object,
final Parameter<T, CouchbasePersistentProperty> parameter) {
return readValue(object, parameter.getType(), parent);
}
}

View File

@@ -16,7 +16,6 @@
package org.springframework.data.couchbase.core.convert.translation;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;

View File

@@ -24,7 +24,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseStorable;
*
* @author Michael Nitschinger
*/
public interface TranslationService<T> {
public interface TranslationService {
/**
* Encodes a {@link CouchbaseDocument} into the target format.
@@ -32,7 +32,7 @@ public interface TranslationService<T> {
* @param source the source document to encode.
* @return the encoded document representation.
*/
T encode(CouchbaseStorable source);
Object encode(CouchbaseStorable source);
/**
* Decodes the target format into a {@link CouchbaseDocument}
@@ -41,5 +41,5 @@ public interface TranslationService<T> {
* @param target the target of the populated data.
* @return a properly populated document to work with.
*/
CouchbaseStorable decode(T source, CouchbaseStorable target);
CouchbaseStorable decode(Object source, CouchbaseStorable target);
}

View File

@@ -0,0 +1,25 @@
package org.springframework.data.couchbase.core.mapping;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public abstract class CouchbaseSimpleTypes {
static {
Set<Class<?>> simpleTypes = new HashSet<Class<?>>();
simpleTypes.add(CouchbaseDocument.class);
simpleTypes.add(CouchbaseList.class);
COUCHBASE_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes);
}
private static final Set<Class<?>> COUCHBASE_SIMPLE_TYPES;
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(COUCHBASE_SIMPLE_TYPES, true);
private CouchbaseSimpleTypes() {
}
}

View File

@@ -0,0 +1,183 @@
/*
* Copyright 2013 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;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.couchbase.TestApplicationConfig;
import org.springframework.data.couchbase.core.convert.CustomConversions;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.*;
import static org.junit.Assert.assertEquals;
/**
* Tests to verify custom mapping logic.
*
* @author Michael Nitschinger
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TestApplicationConfig.class)
public class CustomConvertersTests {
@Autowired
private MappingCouchbaseConverter converter;
@After
public void cleanup() {
converter.setCustomConversions(new CustomConversions(Collections.emptyList()));
}
@Test
public void shouldWriteWithCustomConverter() {
List<Object> converters = new ArrayList<Object>();
converters.add(DateToStringConverter.INSTANCE);
converter.setCustomConversions(new CustomConversions(converters));
converter.afterPropertiesSet();
Date date = new Date();
BlogPost post = new BlogPost();
post.created = date;
CouchbaseDocument doc = new CouchbaseDocument();
converter.write(post, doc);
assertEquals(date.toString(), doc.getPayload().get("created"));
}
@Test
public void shouldReadWithCustomConverter() {
List<Object> converters = new ArrayList<Object>();
converters.add(IntegerToStringConverter.INSTANCE);
converter.setCustomConversions(new CustomConversions(converters));
converter.afterPropertiesSet();
CouchbaseDocument doc = new CouchbaseDocument();
doc.getPayload().put("content", 10);
Counter loaded = converter.read(Counter.class, doc);
assertEquals("even", loaded.content);
}
@Test
public void shouldWriteConvertFullDocument() {
List<Object> converters = new ArrayList<Object>();
converters.add(BlogPostToCouchbaseDocumentConverter.INSTANCE);
converter.setCustomConversions(new CustomConversions(converters));
converter.afterPropertiesSet();
BlogPost post = new BlogPost();
post.id = "foobar";
post.title = "The Foo of the Bar";
CouchbaseDocument doc = new CouchbaseDocument();
converter.write(post, doc);
assertEquals("The Foo of the Bar", doc.getPayload().get("title"));
assertEquals("the_foo_of_the_bar", doc.getPayload().get("slug"));
}
@Test
public void shouldReadConvertFullDocument() {
List<Object> converters = new ArrayList<Object>();
converters.add(CouchbaseDocumentToBlogPostConverter.INSTANCE);
converter.setCustomConversions(new CustomConversions(converters));
converter.afterPropertiesSet();
CouchbaseDocument doc = new CouchbaseDocument();
doc.getPayload().put("title", "My Title");
BlogPost loaded = converter.read(BlogPost.class, doc);
assertEquals("modified", loaded.id);
assertEquals("My Title!!", loaded.title);
}
public static class BlogPost {
@Id
public String id = "key";
@Field
public Date created;
@Field
public String title;
}
public class Counter {
@Field
public String content;
}
public static enum IntegerToStringConverter implements Converter<Integer, String> {
INSTANCE;
@Override
public String convert(Integer source) {
return source % 2 == 0 ? "even" : "odd";
}
}
public static enum DateToStringConverter implements Converter<Date, String> {
INSTANCE;
public static Format FORMATTER = new SimpleDateFormat("yyyy HH");
@Override
public String convert(Date source) {
return source.toString();
}
}
@WritingConverter
public static enum BlogPostToCouchbaseDocumentConverter implements Converter<BlogPost, CouchbaseDocument> {
INSTANCE;
@Override
public CouchbaseDocument convert(BlogPost source) {
return new CouchbaseDocument()
.setId(source.id)
.put("title", source.title)
.put("slug", source.title.toLowerCase().replaceAll(" ", "_"));
}
}
@ReadingConverter
public static enum CouchbaseDocumentToBlogPostConverter implements Converter<CouchbaseDocument, BlogPost> {
INSTANCE;
@Override
public BlogPost convert(CouchbaseDocument source) {
BlogPost post = new BlogPost();
post.id = "modified";
post.title = source.getPayload().get("title") + "!!";
return post;
}
}
}