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 extends CouchbasePersistentEntity>, 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 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 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 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 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;
+ }
+ }
+
+}