diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 6455d8f0..e8a1b633 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -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 bootstrapUris(List hosts) throws URISyntaxException { + private static List bootstrapUris(List hosts) throws URISyntaxException { List uris = new ArrayList(); for (String host : hosts) { uris.add(new URI("http://" + host + ":8091/pools")); diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index c7ac3b91..431faa6e 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -65,7 +65,7 @@ public class CouchbaseTemplate implements CouchbaseOperations { private final CouchbaseClient client; protected final MappingContext, CouchbasePersistentProperty> mappingContext; private final CouchbaseExceptionTranslator exceptionTranslator = new CouchbaseExceptionTranslator(); - private final TranslationService translationService; + private final TranslationService translationService; private CouchbaseConverter couchbaseConverter; private WriteResultChecking writeResultChecking = DEFAULT_WRITE_RESULT_CHECKING; diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java index 785a91b7..f8bb8340 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java @@ -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); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java new file mode 100644 index 00000000..c6ce1e37 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java @@ -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); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java b/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java index 9b1c7c90..b0e14738 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java @@ -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.

* * @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 converters; + + private final Set readingPairs; + private final Set writingPairs; + private final Set> customSimpleTypes; + private final ConcurrentMap 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(); + writingPairs = new LinkedHashSet(); + customSimpleTypes = new HashSet>(); + customReadTargetTypes = new ConcurrentHashMap(); + + this.converters = new ArrayList(); + 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 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); + } + } } 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 4ad194ca..f52255d5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -16,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}. - *

- * 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, - CouchbasePersistentProperty> mappingContext; - protected boolean useFieldAccessOnly = true; - protected CouchbaseTypeMapper typeMapper; - private SpELContext spELContext; + /** + * The generic mapping context. + */ + protected final MappingContext, + 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, - CouchbasePersistentProperty> mappingContext) { + public MappingCouchbaseConverter(final MappingContext, + CouchbasePersistentProperty> mappingContext) { super(ConversionServiceFactory.createDefaultConversionService()); this.mappingContext = mappingContext; typeMapper = new DefaultCouchbaseTypeMapper(DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY); - spELContext = new SpELContext(CouchbaseDocumentPropertyAccessor.INSTANCE); } @Override - public MappingContext, - CouchbasePersistentProperty> getMappingContext() { + public MappingContext, CouchbasePersistentProperty> getMappingContext() { return mappingContext; } @Override - public R read(Class clazz, CouchbaseDocument doc) { - return read(ClassTypeInformation.from(clazz), doc, null); + public R read(final Class clazz, final CouchbaseDocument source) { + return read(ClassTypeInformation.from(clazz), source, null); } - protected R read(TypeInformation 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 the entity type. + * @return the converted entity. + */ + protected R read(final TypeInformation type, final CouchbaseDocument source) { + return read(type, source, null); } - protected R read(TypeInformation 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 the entity type. + * @return the converted entity. + */ + @SuppressWarnings("unchecked") + protected R read(final TypeInformation type, final CouchbaseDocument source, final Object parent) { if (source == null) { return null; } @@ -92,50 +131,60 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter TypeInformation typeToUse = typeMapper.readType(source, type); Class rawType = typeToUse.getType(); + if (conversions.hasCustomReadTarget(source.getClass(), rawType)) { + return conversionService.convert(source, rawType); + } + if (typeToUse.isMap()) { return (R) readMap(typeToUse, source, parent); } - CouchbasePersistentEntity persistentEntity = (CouchbasePersistentEntity) - mappingContext.getPersistentEntity(typeToUse); - - if (persistentEntity == null) { + CouchbasePersistentEntity entity = (CouchbasePersistentEntity) 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 the entity type. + * @return the converted entity. + */ protected R read(final CouchbasePersistentEntity entity, final CouchbaseDocument source, final Object parent) { final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext); - - ParameterValueProvider provider = getParameterProvider(entity, source, evaluator, parent); + ParameterValueProvider 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, R> wrapper = BeanWrapper.create(instance, conversionService); final R result = wrapper.getBean(); entity.doWithProperties(new PropertyHandler() { + @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() { + @Override public void doWithAssociation(final Association 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 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 getParameterProvider( + final CouchbasePersistentEntity entity, final CouchbaseDocument source, + final DefaultSpELExpressionEvaluator evaluator, final Object parent) { CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, evaluator, parent); - PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider( - entity, provider, parent); + PersistentEntityParameterValueProvider parameterProvider = + new PersistentEntityParameterValueProvider(entity, provider, parent); + return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider, - parent); + parent); } - protected Map 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 readMap(final TypeInformation type, final CouchbaseDocument source, + final Object parent) { + Assert.notNull(source); - Class mapType = typeMapper.readType(doc, type).getType(); - Map map = CollectionFactory.createMap(mapType, doc.export().keySet().size()); - Map sourceMap = doc.getPayload(); + Class mapType = typeMapper.readType(source, type).getType(); + Map map = CollectionFactory.createMap(mapType, source.export().keySet().size()); + Map sourceMap = source.getPayload(); for (Map.Entry 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) 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) 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 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, Object> wrapper = BeanWrapper.create(source, conversionService); + final BeanWrapper, 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() { + @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 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 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 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 source, final CouchbaseDocument target, + final TypeInformation type) { for (Map.Entry 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 { - - 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 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 the target type. + * @return the converted object. + */ + @SuppressWarnings("unchecked") private 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 { + + /** + * 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 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 { + SpELExpressionParameterValueProvider { private final Object parent; - public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator, - ConversionService conversionService, ParameterValueProvider delegate, Object parent) { - + public ConverterAwareSpELExpressionParameterValueProvider(final SpELExpressionEvaluator evaluator, + final ConversionService conversionService, final ParameterValueProvider delegate, + final Object parent) { super(evaluator, conversionService, delegate); this.parent = parent; } @Override - protected T potentiallyConvertSpelValue(Object object, Parameter parameter) { + protected T potentiallyConvertSpelValue(final Object object, + final Parameter parameter) { return readValue(object, parameter.getType(), parent); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java index a9d2d320..6c3b5881 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java @@ -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; diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java index 7cf76aef..0c2df4e1 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java @@ -24,7 +24,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseStorable; * * @author Michael Nitschinger */ -public interface TranslationService { +public interface TranslationService { /** * Encodes a {@link CouchbaseDocument} into the target format. @@ -32,7 +32,7 @@ public interface TranslationService { * @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 { * @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); } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java new file mode 100644 index 00000000..7e83efe5 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java @@ -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> simpleTypes = new HashSet>(); + simpleTypes.add(CouchbaseDocument.class); + simpleTypes.add(CouchbaseList.class); + COUCHBASE_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); + } + + private static final Set> COUCHBASE_SIMPLE_TYPES; + public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(COUCHBASE_SIMPLE_TYPES, true); + + private CouchbaseSimpleTypes() { + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java new file mode 100644 index 00000000..fb36e8b4 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java @@ -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 converters = new ArrayList(); + 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 converters = new ArrayList(); + 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 converters = new ArrayList(); + 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 converters = new ArrayList(); + 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 { + INSTANCE; + + @Override + public String convert(Integer source) { + return source % 2 == 0 ? "even" : "odd"; + } + } + + public static enum DateToStringConverter implements Converter { + 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 { + 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 { + INSTANCE; + + @Override + public BlogPost convert(CouchbaseDocument source) { + BlogPost post = new BlogPost(); + post.id = "modified"; + post.title = source.getPayload().get("title") + "!!"; + return post; + } + } + +}