DATACOUCH-55 - Support custom converters.
This changeset adds the possibility to implement custom converters for entities and fields.
This commit is contained in:
@@ -25,6 +25,7 @@ import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.data.annotation.Persistent;
|
||||
import org.springframework.data.couchbase.core.CouchbaseFactoryBean;
|
||||
import org.springframework.data.couchbase.core.CouchbaseTemplate;
|
||||
import org.springframework.data.couchbase.core.convert.CustomConversions;
|
||||
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
|
||||
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
|
||||
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
|
||||
@@ -94,11 +95,11 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
/**
|
||||
* Prepare the logging property before initializing couchbase.
|
||||
*
|
||||
* @param logger
|
||||
* @param logger the logger path to use.
|
||||
*/
|
||||
private void setLoggerProperty(String logger) {
|
||||
private static void setLoggerProperty(final String logger) {
|
||||
Properties systemProperties = System.getProperties();
|
||||
systemProperties.put("net.spy.log.LoggerImpl", logger);
|
||||
systemProperties.setProperty("net.spy.log.LoggerImpl", logger);
|
||||
System.setProperties(systemProperties);
|
||||
}
|
||||
|
||||
@@ -119,7 +120,9 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
*/
|
||||
@Bean
|
||||
public MappingCouchbaseConverter mappingCouchbaseConverter() throws Exception {
|
||||
return new MappingCouchbaseConverter(couchbaseMappingContext());
|
||||
MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext());
|
||||
converter.setCustomConversions(customConversions());
|
||||
return converter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,9 +146,22 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
public CouchbaseMappingContext couchbaseMappingContext() throws Exception {
|
||||
CouchbaseMappingContext mappingContext = new CouchbaseMappingContext();
|
||||
mappingContext.setInitialEntitySet(getInitialEntitySet());
|
||||
mappingContext.setSimpleTypeHolder(customConversions().getSimpleTypeHolder());
|
||||
return mappingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom Converters in a {@link CustomConversions} object if required. These
|
||||
* {@link CustomConversions} will be registered with the {@link #mappingCouchbaseConverter()} and
|
||||
* {@link #couchbaseMappingContext()}. Returns an empty {@link CustomConversions} instance by default.
|
||||
*
|
||||
* @return must not be {@literal null}.
|
||||
*/
|
||||
@Bean
|
||||
public CustomConversions customConversions() {
|
||||
return new CustomConversions(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the mapping base package for classes annotated with {@link Document}.
|
||||
*
|
||||
@@ -188,7 +204,7 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
* @param hosts the list of hosts to convert.
|
||||
* @return the converted URIs.
|
||||
*/
|
||||
private List<URI> bootstrapUris(List<String> hosts) throws URISyntaxException {
|
||||
private static List<URI> bootstrapUris(List<String> hosts) throws URISyntaxException {
|
||||
List<URI> uris = new ArrayList<URI>();
|
||||
for (String host : hosts) {
|
||||
uris.add(new URI("http://" + host + ":8091/pools"));
|
||||
|
||||
@@ -65,7 +65,7 @@ public class CouchbaseTemplate implements CouchbaseOperations {
|
||||
private final CouchbaseClient client;
|
||||
protected final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
|
||||
private final CouchbaseExceptionTranslator exceptionTranslator = new CouchbaseExceptionTranslator();
|
||||
private final TranslationService<String> translationService;
|
||||
private final TranslationService translationService;
|
||||
|
||||
private CouchbaseConverter couchbaseConverter;
|
||||
private WriteResultChecking writeResultChecking = DEFAULT_WRITE_RESULT_CHECKING;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,22 @@
|
||||
|
||||
package org.springframework.data.couchbase.core.convert;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConverterFactory;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.data.convert.JodaTimeConverters;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* Value object to capture custom conversion.
|
||||
@@ -29,14 +40,26 @@ import java.util.List;
|
||||
* inspection nor nested conversion.</p>
|
||||
*
|
||||
* @author Michael Nitschinger
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
public class CustomConversions {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CustomConversions.class);
|
||||
private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a Couchbase supported type! You might wanna check you annotation setup at the converter implementation.";
|
||||
private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a Couchbase supported type! You might wanna check you annotation setup at the converter implementation.";
|
||||
|
||||
/**
|
||||
* Contains the simple type holder.
|
||||
*/
|
||||
private final SimpleTypeHolder simpleTypeHolder;
|
||||
|
||||
private final List<Object> converters;
|
||||
|
||||
private final Set<GenericConverter.ConvertiblePair> readingPairs;
|
||||
private final Set<GenericConverter.ConvertiblePair> writingPairs;
|
||||
private final Set<Class<?>> customSimpleTypes;
|
||||
private final ConcurrentMap<GenericConverter.ConvertiblePair, CacheValue> customReadTargetTypes;
|
||||
|
||||
/**
|
||||
* Create a new instance with no converters.
|
||||
*/
|
||||
@@ -51,6 +74,19 @@ public class CustomConversions {
|
||||
public CustomConversions(final List<?> converters) {
|
||||
Assert.notNull(converters);
|
||||
|
||||
readingPairs = new LinkedHashSet<GenericConverter.ConvertiblePair>();
|
||||
writingPairs = new LinkedHashSet<GenericConverter.ConvertiblePair>();
|
||||
customSimpleTypes = new HashSet<Class<?>>();
|
||||
customReadTargetTypes = new ConcurrentHashMap<GenericConverter.ConvertiblePair, CacheValue>();
|
||||
|
||||
this.converters = new ArrayList<Object>();
|
||||
this.converters.addAll(converters);
|
||||
this.converters.addAll(JodaTimeConverters.getConvertersToRegister());
|
||||
|
||||
for (Object converter : this.converters) {
|
||||
registerConversion(converter);
|
||||
}
|
||||
|
||||
simpleTypeHolder = new SimpleTypeHolder();
|
||||
}
|
||||
|
||||
@@ -64,4 +100,234 @@ public class CustomConversions {
|
||||
return simpleTypeHolder.isSimpleType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the simple type holder.
|
||||
*
|
||||
* @return the simple type holder.
|
||||
*/
|
||||
public SimpleTypeHolder getSimpleTypeHolder() {
|
||||
return simpleTypeHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the given {@link GenericConversionService} with the convertes registered.
|
||||
*
|
||||
* @param conversionService the service to register.
|
||||
*/
|
||||
public void registerConvertersIn(final GenericConversionService conversionService) {
|
||||
for (Object converter : converters) {
|
||||
boolean added = false;
|
||||
|
||||
if (converter instanceof Converter) {
|
||||
conversionService.addConverter((Converter<?, ?>) converter);
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (converter instanceof ConverterFactory) {
|
||||
conversionService.addConverterFactory((ConverterFactory<?, ?>) converter);
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (converter instanceof GenericConverter) {
|
||||
conversionService.addConverter((GenericConverter) converter);
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (!added) {
|
||||
throw new IllegalArgumentException("Given set contains element that is neither Converter nor ConverterFactory!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a conversion for the given converter. Inspects either generics or the convertible pairs returned
|
||||
* by a {@link GenericConverter}.
|
||||
*
|
||||
* @param converter the converter to register.
|
||||
*/
|
||||
private void registerConversion(final Object converter) {
|
||||
Class<?> type = converter.getClass();
|
||||
boolean isWriting = type.isAnnotationPresent(WritingConverter.class);
|
||||
boolean isReading = type.isAnnotationPresent(ReadingConverter.class);
|
||||
|
||||
if (converter instanceof GenericConverter) {
|
||||
GenericConverter genericConverter = (GenericConverter) converter;
|
||||
for (GenericConverter.ConvertiblePair pair : genericConverter.getConvertibleTypes()) {
|
||||
register(new ConverterRegistration(pair, isReading, isWriting));
|
||||
}
|
||||
} else if (converter instanceof Converter) {
|
||||
Class<?>[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), Converter.class);
|
||||
register(new ConverterRegistration(arguments[0], arguments[1], isReading, isWriting));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported Converter type!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given {@link ConverterRegistration} as reading or writing pair depending on the type sides being basic
|
||||
* Couchbase types.
|
||||
*
|
||||
* @param registration the registration.
|
||||
*/
|
||||
private void register(final ConverterRegistration registration) {
|
||||
GenericConverter.ConvertiblePair pair = registration.getConvertiblePair();
|
||||
|
||||
if (registration.isReading()) {
|
||||
readingPairs.add(pair);
|
||||
if (LOG.isWarnEnabled() && !registration.isSimpleSourceType()) {
|
||||
LOG.warn(String.format(READ_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType()));
|
||||
}
|
||||
}
|
||||
|
||||
if (registration.isWriting()) {
|
||||
writingPairs.add(pair);
|
||||
customSimpleTypes.add(pair.getSourceType());
|
||||
if (LOG.isWarnEnabled() && !registration.isSimpleTargetType()) {
|
||||
LOG.warn(String.format(WRITE_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target type to convert to in case we have a custom conversion registered to convert the given source
|
||||
* type into a Couchbase native one.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
public Class<?> getCustomWriteTarget(Class<?> sourceType) {
|
||||
return getCustomWriteTarget(sourceType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target type we can write an object of the given source type to. The returned type might be a subclass
|
||||
* oth the given expected type though. If {@code expectedTargetType} is {@literal null} we will simply return the
|
||||
* first target type matching or {@literal null} if no conversion can be found.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}
|
||||
* @param requestedTargetType
|
||||
* @return
|
||||
*/
|
||||
public Class<?> getCustomWriteTarget(Class<?> sourceType, Class<?> requestedTargetType) {
|
||||
Assert.notNull(sourceType);
|
||||
return getCustomTarget(sourceType, requestedTargetType, writingPairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we have a custom conversion registered to write into a Couchbase native type. The returned type might
|
||||
* be a subclass of the given expected type though.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
public boolean hasCustomWriteTarget(Class<?> sourceType) {
|
||||
Assert.notNull(sourceType);
|
||||
return hasCustomWriteTarget(sourceType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we have a custom conversion registered to write an object of the given source type into an object
|
||||
* of the given Couchbase native target type.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}.
|
||||
* @param requestedTargetType
|
||||
* @return
|
||||
*/
|
||||
public boolean hasCustomWriteTarget(Class<?> sourceType, Class<?> requestedTargetType) {
|
||||
Assert.notNull(sourceType);
|
||||
return getCustomWriteTarget(sourceType, requestedTargetType) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we have a custom conversion registered to read the given source into the given target type.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}
|
||||
* @param requestedTargetType must not be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
public boolean hasCustomReadTarget(Class<?> sourceType, Class<?> requestedTargetType) {
|
||||
Assert.notNull(sourceType);
|
||||
Assert.notNull(requestedTargetType);
|
||||
return getCustomReadTarget(sourceType, requestedTargetType) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual target type for the given {@code sourceType} and {@code requestedTargetType}. Note that the
|
||||
* returned {@link Class} could be an assignable type to the given {@code requestedTargetType}.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}.
|
||||
* @param requestedTargetType can be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private Class<?> getCustomReadTarget(Class<?> sourceType, Class<?> requestedTargetType) {
|
||||
Assert.notNull(sourceType);
|
||||
if (requestedTargetType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
GenericConverter.ConvertiblePair lookupKey = new GenericConverter.ConvertiblePair(sourceType, requestedTargetType);
|
||||
CacheValue readTargetTypeValue = customReadTargetTypes.get(lookupKey);
|
||||
|
||||
if (readTargetTypeValue != null) {
|
||||
return readTargetTypeValue.getType();
|
||||
}
|
||||
|
||||
readTargetTypeValue = CacheValue.of(getCustomTarget(sourceType, requestedTargetType, readingPairs));
|
||||
CacheValue cacheValue = customReadTargetTypes.putIfAbsent(lookupKey, readTargetTypeValue);
|
||||
|
||||
return cacheValue != null ? cacheValue.getType() : readTargetTypeValue.getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects the given {@link org.springframework.core.convert.converter.GenericConverter.ConvertiblePair} for ones
|
||||
* that have a source compatible type as source. Additionally checks assignability of the target type if one is
|
||||
* given.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}.
|
||||
* @param requestedTargetType can be {@literal null}.
|
||||
* @param pairs must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static Class<?> getCustomTarget(Class<?> sourceType, Class<?> requestedTargetType,
|
||||
Iterable<GenericConverter.ConvertiblePair> pairs) {
|
||||
Assert.notNull(sourceType);
|
||||
Assert.notNull(pairs);
|
||||
|
||||
for (GenericConverter.ConvertiblePair typePair : pairs) {
|
||||
if (typePair.getSourceType().isAssignableFrom(sourceType)) {
|
||||
Class<?> targetType = typePair.getTargetType();
|
||||
if (requestedTargetType == null || targetType.isAssignableFrom(requestedTargetType)) {
|
||||
return targetType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to safely store {@literal null} values in the type cache.
|
||||
*
|
||||
* @author Patryk Wasik
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
*/
|
||||
private static class CacheValue {
|
||||
|
||||
private static final CacheValue ABSENT = new CacheValue(null);
|
||||
|
||||
private final Class<?> type;
|
||||
|
||||
public CacheValue(Class<?> type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
static CacheValue of(Class<?> type) {
|
||||
return type == null ? ABSENT : new CacheValue(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.data.couchbase.core.convert;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.CollectionFactory;
|
||||
@@ -41,50 +40,90 @@ import org.springframework.util.CollectionUtils;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* The Couchbase special {@link MappingCouchbaseConverter}.
|
||||
* <p/>
|
||||
* This converter is responsible for mapping (read and writing) value from and to target formats.
|
||||
* A mapping converter for Couchbase.
|
||||
*
|
||||
* The converter is responsible for reading from and writing to entities and converting it into a
|
||||
* consumable database represenation.
|
||||
*
|
||||
* @author Michael Nitschinger
|
||||
*/
|
||||
public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
implements ApplicationContextAware {
|
||||
implements ApplicationContextAware {
|
||||
|
||||
/**
|
||||
* The overall application context.
|
||||
*/
|
||||
protected ApplicationContext applicationContext;
|
||||
protected final MappingContext<? extends CouchbasePersistentEntity<?>,
|
||||
CouchbasePersistentProperty> mappingContext;
|
||||
protected boolean useFieldAccessOnly = true;
|
||||
protected CouchbaseTypeMapper typeMapper;
|
||||
private SpELContext spELContext;
|
||||
|
||||
/**
|
||||
* The generic mapping context.
|
||||
*/
|
||||
protected final MappingContext<? extends CouchbasePersistentEntity<?>,
|
||||
CouchbasePersistentProperty> mappingContext;
|
||||
|
||||
/**
|
||||
* Always use field access only.
|
||||
*/
|
||||
protected boolean useFieldAccessOnly = true;
|
||||
|
||||
/**
|
||||
* The Couchbase specific type mapper in use.
|
||||
*/
|
||||
protected CouchbaseTypeMapper typeMapper;
|
||||
|
||||
/**
|
||||
* Spring Expression Language context.
|
||||
*/
|
||||
private final SpELContext spELContext;
|
||||
|
||||
/**
|
||||
* Create a new {@link MappingCouchbaseConverter}.
|
||||
*
|
||||
* @param mappingContext the mapping context to use.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public MappingCouchbaseConverter(MappingContext<? extends CouchbasePersistentEntity<?>,
|
||||
CouchbasePersistentProperty> mappingContext) {
|
||||
public MappingCouchbaseConverter(final MappingContext<? extends CouchbasePersistentEntity<?>,
|
||||
CouchbasePersistentProperty> mappingContext) {
|
||||
super(ConversionServiceFactory.createDefaultConversionService());
|
||||
|
||||
this.mappingContext = mappingContext;
|
||||
typeMapper = new DefaultCouchbaseTypeMapper(DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY);
|
||||
|
||||
spELContext = new SpELContext(CouchbaseDocumentPropertyAccessor.INSTANCE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MappingContext<? extends CouchbasePersistentEntity<?>,
|
||||
CouchbasePersistentProperty> getMappingContext() {
|
||||
public MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> getMappingContext() {
|
||||
return mappingContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R> R read(Class<R> clazz, CouchbaseDocument doc) {
|
||||
return read(ClassTypeInformation.from(clazz), doc, null);
|
||||
public <R> R read(final Class<R> clazz, final CouchbaseDocument source) {
|
||||
return read(ClassTypeInformation.from(clazz), source, null);
|
||||
}
|
||||
|
||||
protected <R> R read(TypeInformation<R> type, CouchbaseDocument doc) {
|
||||
return read(type, doc, null);
|
||||
/**
|
||||
* Read an incoming {@link CouchbaseDocument} into the target entity.
|
||||
*
|
||||
* @param type the type information of the target entity.
|
||||
* @param source the document to convert.
|
||||
* @param <R> the entity type.
|
||||
* @return the converted entity.
|
||||
*/
|
||||
protected <R> R read(final TypeInformation<R> type, final CouchbaseDocument source) {
|
||||
return read(type, source, null);
|
||||
}
|
||||
|
||||
protected <R> R read(TypeInformation<R> type, final CouchbaseDocument source, Object parent) {
|
||||
|
||||
/**
|
||||
* Read an incoming {@link CouchbaseDocument} into the target entity.
|
||||
*
|
||||
* @param type the type information of the target entity.
|
||||
* @param source the document to convert.
|
||||
* @param parent an optional parent object.
|
||||
* @param <R> the entity type.
|
||||
* @return the converted entity.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <R> R read(final TypeInformation<R> type, final CouchbaseDocument source, final Object parent) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -92,50 +131,60 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
TypeInformation<? extends R> typeToUse = typeMapper.readType(source, type);
|
||||
Class<? extends R> rawType = typeToUse.getType();
|
||||
|
||||
if (conversions.hasCustomReadTarget(source.getClass(), rawType)) {
|
||||
return conversionService.convert(source, rawType);
|
||||
}
|
||||
|
||||
if (typeToUse.isMap()) {
|
||||
return (R) readMap(typeToUse, source, parent);
|
||||
}
|
||||
|
||||
CouchbasePersistentEntity<R> persistentEntity = (CouchbasePersistentEntity<R>)
|
||||
mappingContext.getPersistentEntity(typeToUse);
|
||||
|
||||
if (persistentEntity == null) {
|
||||
CouchbasePersistentEntity<R> entity = (CouchbasePersistentEntity<R>) mappingContext.getPersistentEntity(typeToUse);
|
||||
if (entity == null) {
|
||||
throw new MappingException("No mapping metadata found for " + rawType.getName());
|
||||
}
|
||||
|
||||
return read(persistentEntity, source, parent);
|
||||
return read(entity, source, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an incoming {@link CouchbaseDocument} into the target entity.
|
||||
*
|
||||
* @param entity the target entity.
|
||||
* @param source the document to convert.
|
||||
* @param parent an optional parent object.
|
||||
* @param <R> the entity type.
|
||||
* @return the converted entity.
|
||||
*/
|
||||
protected <R> R read(final CouchbasePersistentEntity<R> entity, final CouchbaseDocument source, final Object parent) {
|
||||
final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext);
|
||||
|
||||
ParameterValueProvider<CouchbasePersistentProperty> provider = getParameterProvider(entity, source, evaluator, parent);
|
||||
ParameterValueProvider<CouchbasePersistentProperty> provider =
|
||||
getParameterProvider(entity, source, evaluator, parent);
|
||||
EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity);
|
||||
R instance = instantiator.createInstance(entity, provider);
|
||||
|
||||
R instance = instantiator.createInstance(entity, provider);
|
||||
final BeanWrapper<CouchbasePersistentEntity<R>, R> wrapper = BeanWrapper.create(instance, conversionService);
|
||||
final R result = wrapper.getBean();
|
||||
|
||||
entity.doWithProperties(new PropertyHandler<CouchbasePersistentProperty>() {
|
||||
@Override
|
||||
public void doWithPersistentProperty(final CouchbasePersistentProperty prop) {
|
||||
if (!doesPropertyExistInSource(prop) || entity.isConstructorArgument(prop)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object obj = prop.isIdProperty() ? source.getId() : getValueInternal(prop, source, evaluator, result);
|
||||
Object obj = prop.isIdProperty() ? source.getId() : getValueInternal(prop, source, result);
|
||||
wrapper.setProperty(prop, obj, useFieldAccessOnly);
|
||||
}
|
||||
|
||||
private boolean doesPropertyExistInSource(CouchbasePersistentProperty property) {
|
||||
private boolean doesPropertyExistInSource(final CouchbasePersistentProperty property) {
|
||||
return property.isIdProperty() || source.containsKey(property.getFieldName());
|
||||
}
|
||||
});
|
||||
|
||||
entity.doWithAssociations(new AssociationHandler<CouchbasePersistentProperty>() {
|
||||
@Override
|
||||
public void doWithAssociation(final Association<CouchbasePersistentProperty> association) {
|
||||
CouchbasePersistentProperty inverseProp = association.getInverse();
|
||||
Object obj = getValueInternal(inverseProp, source, evaluator, result);
|
||||
|
||||
Object obj = getValueInternal(inverseProp, source, result);
|
||||
wrapper.setProperty(inverseProp, obj);
|
||||
}
|
||||
});
|
||||
@@ -143,29 +192,55 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
return result;
|
||||
}
|
||||
|
||||
protected Object getValueInternal(CouchbasePersistentProperty prop, CouchbaseDocument source, SpELExpressionEvaluator eval,
|
||||
Object parent) {
|
||||
|
||||
CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, spELContext, parent);
|
||||
return provider.getPropertyValue(prop);
|
||||
/**
|
||||
* Loads the property value through the value provider.
|
||||
*
|
||||
* @param property the source property.
|
||||
* @param source the source document.
|
||||
* @param parent the optional parent.
|
||||
* @return the actual property value.
|
||||
*/
|
||||
protected Object getValueInternal(final CouchbasePersistentProperty property, final CouchbaseDocument source,
|
||||
final Object parent) {
|
||||
return new CouchbasePropertyValueProvider(source, spELContext, parent).getPropertyValue(property);
|
||||
}
|
||||
|
||||
private ParameterValueProvider<CouchbasePersistentProperty> getParameterProvider(CouchbasePersistentEntity<?> entity,
|
||||
CouchbaseDocument source, DefaultSpELExpressionEvaluator evaluator, Object parent) {
|
||||
|
||||
/**
|
||||
* Creates a new parameter provider.
|
||||
*
|
||||
* @param entity the persistent entity.
|
||||
* @param source the source document.
|
||||
* @param evaluator the SPEL expression evaluator.
|
||||
* @param parent the optional parent.
|
||||
* @return a new parameter value provider.
|
||||
*/
|
||||
private ParameterValueProvider<CouchbasePersistentProperty> getParameterProvider(
|
||||
final CouchbasePersistentEntity<?> entity, final CouchbaseDocument source,
|
||||
final DefaultSpELExpressionEvaluator evaluator, final Object parent) {
|
||||
CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, evaluator, parent);
|
||||
PersistentEntityParameterValueProvider<CouchbasePersistentProperty> parameterProvider = new PersistentEntityParameterValueProvider<CouchbasePersistentProperty>(
|
||||
entity, provider, parent);
|
||||
PersistentEntityParameterValueProvider<CouchbasePersistentProperty> parameterProvider =
|
||||
new PersistentEntityParameterValueProvider<CouchbasePersistentProperty>(entity, provider, parent);
|
||||
|
||||
return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider,
|
||||
parent);
|
||||
parent);
|
||||
}
|
||||
|
||||
protected Map<Object, Object> readMap(TypeInformation<?> type, CouchbaseDocument doc, Object parent) {
|
||||
Assert.notNull(doc);
|
||||
/**
|
||||
* Recursively parses the a map from the source document.
|
||||
*
|
||||
* @param type the type information for the document.
|
||||
* @param source the source document.
|
||||
* @param parent the optional parent.
|
||||
* @return the recursively parsed map.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Map<Object, Object> readMap(final TypeInformation<?> type, final CouchbaseDocument source,
|
||||
final Object parent) {
|
||||
Assert.notNull(source);
|
||||
|
||||
Class<?> mapType = typeMapper.readType(doc, type).getType();
|
||||
Map<Object, Object> map = CollectionFactory.createMap(mapType, doc.export().keySet().size());
|
||||
Map<String, Object> sourceMap = doc.getPayload();
|
||||
Class<?> mapType = typeMapper.readType(source, type).getType();
|
||||
Map<Object, Object> map = CollectionFactory.createMap(mapType, source.export().keySet().size());
|
||||
Map<String, Object> sourceMap = source.getPayload();
|
||||
|
||||
for (Map.Entry<String, Object> entry : sourceMap.entrySet()) {
|
||||
Object key = entry.getKey();
|
||||
@@ -178,7 +253,6 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
}
|
||||
|
||||
TypeInformation<?> valueType = type.getMapValueType();
|
||||
|
||||
if (value instanceof CouchbaseDocument) {
|
||||
map.put(key, read(valueType, (CouchbaseDocument) value, parent));
|
||||
} else if (value instanceof CouchbaseList) {
|
||||
@@ -192,12 +266,23 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
return map;
|
||||
}
|
||||
|
||||
private Object getPotentiallyConvertedSimpleRead(Object value, Class<?> target) {
|
||||
|
||||
/**
|
||||
* Potentially convert simple values like ENUMs.
|
||||
*
|
||||
* @param value the value to convert.
|
||||
* @param target the target object.
|
||||
* @return the potentially converted object.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object getPotentiallyConvertedSimpleRead(final Object value, final Class<?> target) {
|
||||
if (value == null || target == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (conversions.hasCustomReadTarget(value.getClass(), target)) {
|
||||
return conversionService.convert(value, target);
|
||||
}
|
||||
|
||||
if (Enum.class.isAssignableFrom(target)) {
|
||||
return Enum.valueOf((Class<Enum>) target, value.toString());
|
||||
}
|
||||
@@ -219,20 +304,38 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isCustom = conversions.getCustomWriteTarget(source.getClass(), CouchbaseDocument.class) != null;
|
||||
TypeInformation<?> type = ClassTypeInformation.from(source.getClass());
|
||||
typeMapper.writeType(type, target);
|
||||
writeInternal(source, target, type);
|
||||
|
||||
if (!isCustom) {
|
||||
typeMapper.writeType(type, target);
|
||||
}
|
||||
|
||||
writeInternal(source, target, type);
|
||||
if (target.getId() == null) {
|
||||
throw new MappingException("An ID property is needed, but not found on this entity.");
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeInternal(final Object source, final CouchbaseDocument target, final TypeInformation<?> typeHint) {
|
||||
/**
|
||||
* Convert a source object into a {@link CouchbaseDocument} target.
|
||||
*
|
||||
* @param source the source object.
|
||||
* @param target the target document.
|
||||
* @param typeHint the type information for the source.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void writeInternal(final Object source, CouchbaseDocument target, final TypeInformation<?> typeHint) {
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Class<?> customTarget = conversions.getCustomWriteTarget(source.getClass(), CouchbaseDocument.class);
|
||||
if (customTarget != null) {
|
||||
copyCouchbaseDocument(conversionService.convert(source, CouchbaseDocument.class), target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Map.class.isAssignableFrom(source.getClass())) {
|
||||
writeMapInternal((Map<Object, Object>) source, target, ClassTypeInformation.MAP);
|
||||
return;
|
||||
@@ -247,7 +350,29 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
addCustomTypeKeyIfNecessary(typeHint, source, target);
|
||||
}
|
||||
|
||||
protected void writeInternal(final Object source, final CouchbaseDocument target, final CouchbasePersistentEntity<?> entity) {
|
||||
/**
|
||||
* Helper method to copy the internals from a source document into a target document.
|
||||
*
|
||||
* @param source the source document.
|
||||
* @param target the target document.
|
||||
*/
|
||||
protected void copyCouchbaseDocument(final CouchbaseDocument source, final CouchbaseDocument target) {
|
||||
for (Map.Entry<String, Object> entry : source.export().entrySet()) {
|
||||
target.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
target.setId(source.getId());
|
||||
target.setExpiration(source.getExpiration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper method to write the source object into the target document.
|
||||
*
|
||||
* @param source the source object.
|
||||
* @param target the target document.
|
||||
* @param entity the persistent entity to convert from.
|
||||
*/
|
||||
protected void writeInternal(final Object source, final CouchbaseDocument target,
|
||||
final CouchbasePersistentEntity<?> entity) {
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
@@ -256,7 +381,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
throw new MappingException("No mapping metadata found for entity of type " + source.getClass().getName());
|
||||
}
|
||||
|
||||
final BeanWrapper<CouchbasePersistentEntity<Object>, Object> wrapper = BeanWrapper.create(source, conversionService);
|
||||
final BeanWrapper<CouchbasePersistentEntity<Object>, Object> wrapper = BeanWrapper.create(source,
|
||||
conversionService);
|
||||
final CouchbasePersistentProperty idProperty = entity.getIdProperty();
|
||||
final CouchbasePersistentProperty versionProperty = entity.getVersionProperty();
|
||||
|
||||
@@ -267,11 +393,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
target.setExpiration(entity.getExpiry());
|
||||
|
||||
entity.doWithProperties(new PropertyHandler<CouchbasePersistentProperty>() {
|
||||
@Override
|
||||
public void doWithPersistentProperty(final CouchbasePersistentProperty prop) {
|
||||
if (prop.equals(idProperty)) {
|
||||
return;
|
||||
}
|
||||
if (versionProperty != null && prop.equals(versionProperty)) {
|
||||
if (prop.equals(idProperty) || (versionProperty != null && prop.equals(versionProperty))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -300,7 +424,16 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
|
||||
}
|
||||
|
||||
private void writePropertyInternal(final Object source, final CouchbaseDocument target, final CouchbasePersistentProperty prop) {
|
||||
/**
|
||||
* Helper method to write a property into the target document.
|
||||
*
|
||||
* @param source the source object.
|
||||
* @param target the target document.
|
||||
* @param prop the property information.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void writePropertyInternal(final Object source, final CouchbaseDocument target,
|
||||
final CouchbasePersistentProperty prop) {
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
@@ -321,23 +454,45 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
return;
|
||||
}
|
||||
|
||||
Class<?> basicTargetType = conversions.getCustomWriteTarget(source.getClass(), null);
|
||||
if (basicTargetType != null) {
|
||||
target.put(name, conversionService.convert(source, basicTargetType));
|
||||
return;
|
||||
}
|
||||
|
||||
CouchbaseDocument propertyDoc = new CouchbaseDocument();
|
||||
addCustomTypeKeyIfNecessary(type, source, propertyDoc);
|
||||
|
||||
CouchbasePersistentEntity<?> entity = isSubtype(prop.getType(), source.getClass()) ? mappingContext
|
||||
.getPersistentEntity(source.getClass()) : mappingContext.getPersistentEntity(type);
|
||||
.getPersistentEntity(source.getClass()) : mappingContext.getPersistentEntity(type);
|
||||
writeInternal(source, propertyDoc, entity);
|
||||
target.put(name, propertyDoc);
|
||||
}
|
||||
|
||||
private CouchbaseDocument createMap(Map<Object, Object> map, CouchbasePersistentProperty prop) {
|
||||
/**
|
||||
* Wrapper method to create the underlying map.
|
||||
*
|
||||
* @param map the source map.
|
||||
* @param prop the persistent property.
|
||||
* @return the written couchbase document.
|
||||
*/
|
||||
private CouchbaseDocument createMap(final Map<Object, Object> map, final CouchbasePersistentProperty prop) {
|
||||
Assert.notNull(map, "Given map must not be null!");
|
||||
Assert.notNull(prop, "PersistentProperty must not be null!");
|
||||
|
||||
return writeMapInternal(map, new CouchbaseDocument(), prop.getTypeInformation());
|
||||
}
|
||||
|
||||
private CouchbaseDocument writeMapInternal(Map<Object, Object> source, CouchbaseDocument target, TypeInformation<?> type) {
|
||||
/**
|
||||
* Helper method to write the map into the couchbase document.
|
||||
*
|
||||
* @param source the source object.
|
||||
* @param target the target document.
|
||||
* @param type the type information for the document.
|
||||
* @return the written couchbase document.
|
||||
*/
|
||||
private CouchbaseDocument writeMapInternal(final Map<Object, Object> source, final CouchbaseDocument target,
|
||||
final TypeInformation<?> type) {
|
||||
for (Map.Entry<Object, Object> entry : source.entrySet()) {
|
||||
Object key = entry.getKey();
|
||||
Object val = entry.getValue();
|
||||
@@ -363,11 +518,27 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
return target;
|
||||
}
|
||||
|
||||
private CouchbaseList createCollection(Collection<?> collection, CouchbasePersistentProperty prop) {
|
||||
/**
|
||||
* Helper method to create the underlying collection/list.
|
||||
*
|
||||
* @param collection the collection to write.
|
||||
* @param prop the property information.
|
||||
* @return the created couchbase list.
|
||||
*/
|
||||
private CouchbaseList createCollection(final Collection<?> collection, final CouchbasePersistentProperty prop) {
|
||||
return writeCollectionInternal(collection, new CouchbaseList(), prop.getTypeInformation());
|
||||
}
|
||||
|
||||
private CouchbaseList writeCollectionInternal(Collection<?> source, CouchbaseList target, TypeInformation<?> type) {
|
||||
/**
|
||||
* Helper method to write the internal collection.
|
||||
*
|
||||
* @param source the source object.
|
||||
* @param target the target document.
|
||||
* @param type the type information for the document.
|
||||
* @return the created couchbase list.
|
||||
*/
|
||||
private CouchbaseList writeCollectionInternal(final Collection<?> source, final CouchbaseList target,
|
||||
final TypeInformation<?> type) {
|
||||
TypeInformation<?> componentType = type == null ? null : type.getComponentType();
|
||||
|
||||
for (Object element : source) {
|
||||
@@ -388,6 +559,15 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a collection from the source object.
|
||||
*
|
||||
* @param targetType the target type.
|
||||
* @param source the list as source.
|
||||
* @param parent the optional parent.
|
||||
* @return the instantiated collection.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object readCollection(final TypeInformation<?> targetType, final CouchbaseList source, final Object parent) {
|
||||
Assert.notNull(targetType);
|
||||
|
||||
@@ -418,9 +598,13 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
return getPotentiallyConvertedSimpleRead(items, targetType.getType());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a collection from the given source object.
|
||||
*
|
||||
* @param source the source object.
|
||||
* @return the target collection.
|
||||
*/
|
||||
private static Collection<?> asCollection(final Object source) {
|
||||
|
||||
if (source instanceof Collection) {
|
||||
return (Collection<?>) source;
|
||||
}
|
||||
@@ -428,14 +612,48 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
|
||||
}
|
||||
|
||||
private boolean isSubtype(final Class<?> left, final Class<?> right) {
|
||||
/**
|
||||
* Check if one class is a subtype of the other.
|
||||
*
|
||||
* @param left the first class.
|
||||
* @param right the second class.
|
||||
* @return true if it is a subtype, false otherwise.
|
||||
*/
|
||||
private static boolean isSubtype(final Class<?> left, final Class<?> right) {
|
||||
return left.isAssignableFrom(right) && !left.equals(right);
|
||||
}
|
||||
|
||||
private void writeSimpleInternal(Object source, CouchbaseDocument target, String key) {
|
||||
target.put(key, source);
|
||||
/**
|
||||
* Write the given source into the couchbase document target.
|
||||
*
|
||||
* @param source the source object.
|
||||
* @param target the target document.
|
||||
* @param key the key of the object.
|
||||
*/
|
||||
private void writeSimpleInternal(final Object source, final CouchbaseDocument target, final String key) {
|
||||
target.put(key, getPotentiallyConvertedSimpleWrite(source));
|
||||
}
|
||||
|
||||
private Object getPotentiallyConvertedSimpleWrite(final Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Class<?> customTarget = conversions.getCustomWriteTarget(value.getClass(), null);
|
||||
if (customTarget != null) {
|
||||
return conversionService.convert(value, customTarget);
|
||||
} else {
|
||||
return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom type key if needed.
|
||||
*
|
||||
* @param type the type information.
|
||||
* @param source th the source object.
|
||||
* @param target the target document.
|
||||
*/
|
||||
protected void addCustomTypeKeyIfNecessary(TypeInformation<?> type, Object source, CouchbaseDocument target) {
|
||||
TypeInformation<?> actualType = type != null ? type.getActualType() : type;
|
||||
Class<?> reference = actualType == null ? Object.class : actualType.getType();
|
||||
@@ -447,52 +665,26 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext)
|
||||
throws BeansException {
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
private class CouchbasePropertyValueProvider implements PropertyValueProvider<CouchbasePersistentProperty> {
|
||||
|
||||
private final CouchbaseDocument source;
|
||||
private final SpELExpressionEvaluator evaluator;
|
||||
private final Object parent;
|
||||
|
||||
public CouchbasePropertyValueProvider(CouchbaseDocument source, SpELContext factory, Object parent) {
|
||||
this(source, new DefaultSpELExpressionEvaluator(source, factory), parent);
|
||||
}
|
||||
|
||||
public CouchbasePropertyValueProvider(CouchbaseDocument source, DefaultSpELExpressionEvaluator evaluator, Object parent) {
|
||||
|
||||
Assert.notNull(source);
|
||||
Assert.notNull(evaluator);
|
||||
|
||||
this.source = source;
|
||||
this.evaluator = evaluator;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public <R> R getPropertyValue(final CouchbasePersistentProperty property) {
|
||||
|
||||
String expression = property.getSpelExpression();
|
||||
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName());
|
||||
|
||||
if (property.isIdProperty()) {
|
||||
return (R) source.getId();
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return readValue(value, property.getTypeInformation(), parent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to read the value based on the value type.
|
||||
*
|
||||
* @param value the value to convert.
|
||||
* @param type the type information.
|
||||
* @param parent the optional parent.
|
||||
* @param <R> the target type.
|
||||
* @return the converted object.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <R> R readValue(Object value, TypeInformation<?> type, Object parent) {
|
||||
Class<?> rawType = type.getType();
|
||||
|
||||
if (value instanceof CouchbaseDocument) {
|
||||
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
|
||||
return (R) conversionService.convert(value, rawType);
|
||||
} else if (value instanceof CouchbaseDocument) {
|
||||
return (R) read(type, (CouchbaseDocument) value, parent);
|
||||
} else if (value instanceof CouchbaseList) {
|
||||
return (R) readCollection(type, (CouchbaseList) value, parent);
|
||||
@@ -501,20 +693,76 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property value provider for Couchbase documents.
|
||||
*/
|
||||
private class CouchbasePropertyValueProvider implements PropertyValueProvider<CouchbasePersistentProperty> {
|
||||
|
||||
/**
|
||||
* The source document.
|
||||
*/
|
||||
private final CouchbaseDocument source;
|
||||
|
||||
/**
|
||||
* The expression evaluator.
|
||||
*/
|
||||
private final SpELExpressionEvaluator evaluator;
|
||||
|
||||
/**
|
||||
* The optional parent object.
|
||||
*/
|
||||
private final Object parent;
|
||||
|
||||
public CouchbasePropertyValueProvider(final CouchbaseDocument source, final SpELContext factory,
|
||||
final Object parent) {
|
||||
this(source, new DefaultSpELExpressionEvaluator(source, factory), parent);
|
||||
}
|
||||
|
||||
public CouchbasePropertyValueProvider(final CouchbaseDocument source,
|
||||
final DefaultSpELExpressionEvaluator evaluator, final Object parent) {
|
||||
Assert.notNull(source);
|
||||
Assert.notNull(evaluator);
|
||||
|
||||
this.source = source;
|
||||
this.evaluator = evaluator;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <R> R getPropertyValue(final CouchbasePersistentProperty property) {
|
||||
String expression = property.getSpelExpression();
|
||||
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName());
|
||||
|
||||
if (property.isIdProperty()) {
|
||||
return (R) source.getId();
|
||||
}
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return readValue(value, property.getTypeInformation(), parent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A expression parameter value provider.
|
||||
*/
|
||||
private class ConverterAwareSpELExpressionParameterValueProvider extends
|
||||
SpELExpressionParameterValueProvider<CouchbasePersistentProperty> {
|
||||
SpELExpressionParameterValueProvider<CouchbasePersistentProperty> {
|
||||
|
||||
private final Object parent;
|
||||
|
||||
public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator,
|
||||
ConversionService conversionService, ParameterValueProvider<CouchbasePersistentProperty> delegate, Object parent) {
|
||||
|
||||
public ConverterAwareSpELExpressionParameterValueProvider(final SpELExpressionEvaluator evaluator,
|
||||
final ConversionService conversionService, final ParameterValueProvider<CouchbasePersistentProperty> delegate,
|
||||
final Object parent) {
|
||||
super(evaluator, conversionService, delegate);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <T> T potentiallyConvertSpelValue(Object object, Parameter<T, CouchbasePersistentProperty> parameter) {
|
||||
protected <T> T potentiallyConvertSpelValue(final Object object,
|
||||
final Parameter<T, CouchbasePersistentProperty> parameter) {
|
||||
return readValue(object, parameter.getType(), parent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseStorable;
|
||||
*
|
||||
* @author Michael Nitschinger
|
||||
*/
|
||||
public interface TranslationService<T> {
|
||||
public interface TranslationService {
|
||||
|
||||
/**
|
||||
* Encodes a {@link CouchbaseDocument} into the target format.
|
||||
@@ -32,7 +32,7 @@ public interface TranslationService<T> {
|
||||
* @param source the source document to encode.
|
||||
* @return the encoded document representation.
|
||||
*/
|
||||
T encode(CouchbaseStorable source);
|
||||
Object encode(CouchbaseStorable source);
|
||||
|
||||
/**
|
||||
* Decodes the target format into a {@link CouchbaseDocument}
|
||||
@@ -41,5 +41,5 @@ public interface TranslationService<T> {
|
||||
* @param target the target of the populated data.
|
||||
* @return a properly populated document to work with.
|
||||
*/
|
||||
CouchbaseStorable decode(T source, CouchbaseStorable target);
|
||||
CouchbaseStorable decode(Object source, CouchbaseStorable target);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.springframework.data.couchbase.core.mapping;
|
||||
|
||||
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class CouchbaseSimpleTypes {
|
||||
|
||||
static {
|
||||
Set<Class<?>> simpleTypes = new HashSet<Class<?>>();
|
||||
simpleTypes.add(CouchbaseDocument.class);
|
||||
simpleTypes.add(CouchbaseList.class);
|
||||
COUCHBASE_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes);
|
||||
}
|
||||
|
||||
private static final Set<Class<?>> COUCHBASE_SIMPLE_TYPES;
|
||||
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(COUCHBASE_SIMPLE_TYPES, true);
|
||||
|
||||
private CouchbaseSimpleTypes() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright 2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.data.couchbase.core.mapping;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.couchbase.TestApplicationConfig;
|
||||
import org.springframework.data.couchbase.core.convert.CustomConversions;
|
||||
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.text.Format;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Tests to verify custom mapping logic.
|
||||
*
|
||||
* @author Michael Nitschinger
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration(classes = TestApplicationConfig.class)
|
||||
public class CustomConvertersTests {
|
||||
|
||||
@Autowired
|
||||
private MappingCouchbaseConverter converter;
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
converter.setCustomConversions(new CustomConversions(Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldWriteWithCustomConverter() {
|
||||
List<Object> converters = new ArrayList<Object>();
|
||||
converters.add(DateToStringConverter.INSTANCE);
|
||||
converter.setCustomConversions(new CustomConversions(converters));
|
||||
converter.afterPropertiesSet();
|
||||
|
||||
Date date = new Date();
|
||||
BlogPost post = new BlogPost();
|
||||
post.created = date;
|
||||
|
||||
CouchbaseDocument doc = new CouchbaseDocument();
|
||||
converter.write(post, doc);
|
||||
|
||||
assertEquals(date.toString(), doc.getPayload().get("created"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadWithCustomConverter() {
|
||||
List<Object> converters = new ArrayList<Object>();
|
||||
converters.add(IntegerToStringConverter.INSTANCE);
|
||||
converter.setCustomConversions(new CustomConversions(converters));
|
||||
converter.afterPropertiesSet();
|
||||
|
||||
CouchbaseDocument doc = new CouchbaseDocument();
|
||||
doc.getPayload().put("content", 10);
|
||||
Counter loaded = converter.read(Counter.class, doc);
|
||||
assertEquals("even", loaded.content);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldWriteConvertFullDocument() {
|
||||
List<Object> converters = new ArrayList<Object>();
|
||||
converters.add(BlogPostToCouchbaseDocumentConverter.INSTANCE);
|
||||
converter.setCustomConversions(new CustomConversions(converters));
|
||||
converter.afterPropertiesSet();
|
||||
|
||||
BlogPost post = new BlogPost();
|
||||
post.id = "foobar";
|
||||
post.title = "The Foo of the Bar";
|
||||
|
||||
CouchbaseDocument doc = new CouchbaseDocument();
|
||||
converter.write(post, doc);
|
||||
|
||||
assertEquals("The Foo of the Bar", doc.getPayload().get("title"));
|
||||
assertEquals("the_foo_of_the_bar", doc.getPayload().get("slug"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldReadConvertFullDocument() {
|
||||
List<Object> converters = new ArrayList<Object>();
|
||||
converters.add(CouchbaseDocumentToBlogPostConverter.INSTANCE);
|
||||
converter.setCustomConversions(new CustomConversions(converters));
|
||||
converter.afterPropertiesSet();
|
||||
|
||||
CouchbaseDocument doc = new CouchbaseDocument();
|
||||
doc.getPayload().put("title", "My Title");
|
||||
|
||||
BlogPost loaded = converter.read(BlogPost.class, doc);
|
||||
assertEquals("modified", loaded.id);
|
||||
assertEquals("My Title!!", loaded.title);
|
||||
}
|
||||
|
||||
public static class BlogPost {
|
||||
@Id
|
||||
public String id = "key";
|
||||
|
||||
@Field
|
||||
public Date created;
|
||||
|
||||
@Field
|
||||
public String title;
|
||||
|
||||
}
|
||||
|
||||
public class Counter {
|
||||
@Field
|
||||
public String content;
|
||||
}
|
||||
|
||||
public static enum IntegerToStringConverter implements Converter<Integer, String> {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public String convert(Integer source) {
|
||||
return source % 2 == 0 ? "even" : "odd";
|
||||
}
|
||||
}
|
||||
|
||||
public static enum DateToStringConverter implements Converter<Date, String> {
|
||||
INSTANCE;
|
||||
|
||||
public static Format FORMATTER = new SimpleDateFormat("yyyy HH");
|
||||
|
||||
@Override
|
||||
public String convert(Date source) {
|
||||
return source.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
public static enum BlogPostToCouchbaseDocumentConverter implements Converter<BlogPost, CouchbaseDocument> {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public CouchbaseDocument convert(BlogPost source) {
|
||||
return new CouchbaseDocument()
|
||||
.setId(source.id)
|
||||
.put("title", source.title)
|
||||
.put("slug", source.title.toLowerCase().replaceAll(" ", "_"));
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
public static enum CouchbaseDocumentToBlogPostConverter implements Converter<CouchbaseDocument, BlogPost> {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public BlogPost convert(CouchbaseDocument source) {
|
||||
BlogPost post = new BlogPost();
|
||||
post.id = "modified";
|
||||
post.title = source.getPayload().get("title") + "!!";
|
||||
return post;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user