Merge pull request #44 from simonbasle/converter

[SDK2] added back the convert and mapping packages
This commit is contained in:
Simon Baslé
2015-06-29 16:37:07 +02:00
21 changed files with 3008 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2012-2015 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.beans.factory.InitializingBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.EntityInstantiators;
/**
* An abstract {@link CouchbaseConverter} that provides the basics for the {@link MappingCouchbaseConverter}.
*
* @author Michael Nitschinger
*/
public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, InitializingBean {
/**
* Contains the conversion service.
*/
protected final GenericConversionService conversionService;
/**
* Contains the entity instantiators.
*/
protected EntityInstantiators instantiators = new EntityInstantiators();
/**
* Holds the custom conversions.
*/
protected CustomConversions conversions = new CustomConversions();
/**
* Create a new converter and hand it over the {@link ConversionService}
*
* @param conversionService the conversion service to use.
*/
protected AbstractCouchbaseConverter(final GenericConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* Return the conversion service.
*
* @return the conversion service.
*/
@Override
public ConversionService getConversionService() {
return conversionService;
}
/**
* Set the custom conversions.
*
* @param conversions the conversions.
*/
public void setCustomConversions(final CustomConversions conversions) {
this.conversions = conversions;
}
/**
* Set the entity instantiators.
*
* @param instantiators the instantiators.
*/
public void setInstantiators(final EntityInstantiators instantiators) {
this.instantiators = instantiators;
}
/**
* Do nothing after the properties set on the bean.
*/
@Override
public void afterPropertiesSet() {
conversions.registerConvertersIn(conversionService);
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2012-2015 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.convert;
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
import org.springframework.data.couchbase.core.mapping.CouchbaseSimpleTypes;
import org.springframework.util.Assert;
/**
* Conversion registration information.
*
* @author Oliver Gierke
* @author Michael Nitschinger
*/
class ConverterRegistration {
private final ConvertiblePair convertiblePair;
private final boolean reading;
private final boolean writing;
/**
* Creates a new {@link ConverterRegistration}.
*
* @param convertiblePair must not be {@literal null}.
* @param isReading whether to force to consider the converter for reading.
* @param isWriting whether to force to consider the converter for reading.
*/
public ConverterRegistration(ConvertiblePair convertiblePair, boolean isReading, boolean isWriting) {
Assert.notNull(convertiblePair);
this.convertiblePair = convertiblePair;
reading = isReading;
writing = isWriting;
}
/**
* Creates a new {@link ConverterRegistration} from the given source and target type and read/write flags.
*
* @param source the source type to be converted from, must not be {@literal null}.
* @param target the target type to be converted to, must not be {@literal null}.
* @param isReading whether to force to consider the converter for reading.
* @param isWriting whether to force to consider the converter for writing.
*/
public ConverterRegistration(Class<?> source, Class<?> target, boolean isReading, boolean isWriting) {
this(new ConvertiblePair(source, target), isReading, isWriting);
}
/**
* Returns whether the converter shall be used for writing.
*
* @return
*/
public boolean isWriting() {
return writing == true || (!reading && isSimpleTargetType());
}
/**
* Returns whether the converter shall be used for reading.
*
* @return
*/
public boolean isReading() {
return reading == true || (!writing && isSimpleSourceType());
}
/**
* Returns the actual conversion pair.
*
* @return
*/
public ConvertiblePair getConvertiblePair() {
return convertiblePair;
}
/**
* Returns whether the source type is a Mongo simple one.
*
* @return
*/
public boolean isSimpleSourceType() {
return isCouchbaseBasicType(convertiblePair.getSourceType());
}
/**
* Returns whether the target type is a Mongo simple one.
*
* @return
*/
public boolean isSimpleTargetType() {
return isCouchbaseBasicType(convertiblePair.getTargetType());
}
/**
* Returns whether the given type is a type that Mongo can handle basically.
*
* @param type
* @return
*/
private static boolean isCouchbaseBasicType(Class<?> type) {
return CouchbaseSimpleTypes.HOLDER.isSimpleType(type);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2015 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.data.convert.EntityConverter;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
/**
* Marker interface for the converter, identifying the types to and from that can be converted.
*
* @author Michael Nitschinger
*/
public interface CouchbaseConverter
extends EntityConverter<CouchbasePersistentEntity<?>,
CouchbasePersistentProperty, Object, CouchbaseDocument>,
CouchbaseWriter<Object, CouchbaseDocument>,
EntityReader<Object, CouchbaseDocument> {
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2012-2015 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 java.util.Map;
import org.springframework.context.expression.MapAccessor;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.TypedValue;
/**
* A property accessor for document properties.
*
* @author Michael Nitschinger
*/
public class CouchbaseDocumentPropertyAccessor extends MapAccessor {
/**
* Contains the static instance of thi accessor.
*/
static final MapAccessor INSTANCE = new CouchbaseDocumentPropertyAccessor();
/**
* Returns the target classes of the properties.
*
* @return
*/
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class[] {CouchbaseDocument.class};
}
/**
* It can always read from those properties.
*
* @param context the evaluation context.
* @param target the target object.
* @param name the name of the property.
* @return always true.
*/
@Override
public boolean canRead(final EvaluationContext context, final Object target, final String name) {
return true;
}
/**
* Read the value from the property.
*
* @param context the evaluation context.
* @param target the target object.
* @param name the name of the property.
* @return the typed value of the content to be read.
*/
@Override
public TypedValue read(final EvaluationContext context, final Object target, final String name) {
Map<String, Object> source = (Map<String, Object>) target;
Object value = source.get(name);
return value == null ? TypedValue.NULL : new TypedValue(value);
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2012-2015 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.data.convert.TypeMapper;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
/**
* Marker interface for the TypeMapper.
*
* @author Michael Nitschinger
*/
public interface CouchbaseTypeMapper extends TypeMapper<CouchbaseDocument> {
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2012-2015 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.data.convert.EntityWriter;
/**
* Marker interface for the Couchbase {@link EntityWriter}.
*
* @author Michael Nitschinger
*/
public interface CouchbaseWriter<T, ConvertedCouchbaseDocument> extends
EntityWriter<T, ConvertedCouchbaseDocument> {
}

View File

@@ -0,0 +1,340 @@
/*
* Copyright 2012-2015 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 java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
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.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.util.Assert;
/**
* Value object to capture custom conversion.
* <p/>
* <p>Types that can be mapped directly onto JSON are considered simple ones, because they neither need deeper
* 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.
*/
CustomConversions() {
this(new ArrayList<Object>());
}
/**
* Create a new instance with a given list of conversers.
*
* @param converters the list of custom converters.
*/
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(DateConverters.getConvertersToRegister());
for (Object converter : this.converters) {
registerConversion(converter);
}
simpleTypeHolder = new SimpleTypeHolder(customSimpleTypes, true);
}
/**
* Check that the given type is of "simple type".
*
* @param type the type to check.
* @return if its simple type or not.
*/
public boolean isSimpleType(final Class<?> type) {
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 GenericConverter.ConvertiblePair} for ones
* that have a source compatible type as source. Additionally checks assignability of the target type if one is
* given.
*
* @param sourceType must not be {@literal null}.
* @param requestedTargetType can be {@literal null}.
* @param pairs must not be {@literal null}.
* @return
*/
private static Class<?> getCustomTarget(Class<?> sourceType, Class<?> requestedTargetType,
Iterable<GenericConverter.ConvertiblePair> pairs) {
Assert.notNull(sourceType);
Assert.notNull(pairs);
for (GenericConverter.ConvertiblePair typePair : pairs) {
if (typePair.getSourceType().isAssignableFrom(sourceType)) {
Class<?> targetType = typePair.getTargetType();
if (requestedTargetType == null || targetType.isAssignableFrom(requestedTargetType)) {
return targetType;
}
}
}
return null;
}
/**
* Wrapper to safely store {@literal null} values in the type cache.
*
* @author Patryk Wasik
* @author Oliver Gierke
* @author Thomas Darimont
*/
private static class CacheValue {
private static final CacheValue ABSENT = new CacheValue(null);
private final Class<?> type;
public CacheValue(Class<?> type) {
this.type = type;
}
public Class<?> getType() {
return type;
}
static CacheValue of(Class<?> type) {
return type == null ? ABSENT : new CacheValue(type);
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright 2012-2015 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 java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.joda.time.DateMidnight;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.util.ClassUtils;
/**
* Out of the box conversions for java dates and calendars.
*
* @author Michael Nitschinger
*/
public final class DateConverters {
private DateConverters() {
}
private static final boolean JODA_TIME_IS_PRESENT = ClassUtils.isPresent("org.joda.time.LocalDate", null);
/**
* Returns all converters by this class that can be registered.
*
* @return the list of converters to register.
*/
public static Collection<Converter<?, ?>> getConvertersToRegister() {
List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
converters.add(DateToLongConverter.INSTANCE);
converters.add(CalendarToLongConverter.INSTANCE);
converters.add(LongToDateConverter.INSTANCE);
converters.add(LongToCalendarConverter.INSTANCE);
if (JODA_TIME_IS_PRESENT) {
converters.add(LocalDateToLongConverter.INSTANCE);
converters.add(LocalDateTimeToLongConverter.INSTANCE);
converters.add(DateTimeToLongConverter.INSTANCE);
converters.add(DateMidnightToLongConverter.INSTANCE);
converters.add(LongToLocalDateConverter.INSTANCE);
converters.add(LongToLocalDateTimeConverter.INSTANCE);
converters.add(LongToDateTimeConverter.INSTANCE);
converters.add(LongToDateMidnightConverter.INSTANCE);
}
return converters;
}
@WritingConverter
public enum DateToLongConverter implements Converter<Date, Long> {
INSTANCE;
@Override
public Long convert(Date source) {
return source == null ? null : source.getTime();
}
}
@WritingConverter
public enum CalendarToLongConverter implements Converter<Calendar, Long> {
INSTANCE;
@Override
public Long convert(Calendar source) {
return source == null ? null : source.getTimeInMillis() / 1000;
}
}
@ReadingConverter
public enum LongToDateConverter implements Converter<Long, Date> {
INSTANCE;
@Override
public Date convert(Long source) {
if (source == null) {
return null;
}
Date date = new Date();
date.setTime(source);
return date;
}
}
@ReadingConverter
public enum LongToCalendarConverter implements Converter<Long, Calendar> {
INSTANCE;
@Override
public Calendar convert(Long source) {
if (source == null) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(source * 1000);
return calendar;
}
}
@WritingConverter
public enum LocalDateToLongConverter implements Converter<LocalDate, Long> {
INSTANCE;
@Override
public Long convert(LocalDate source) {
return source == null ? null : source.toDate().getTime();
}
}
@WritingConverter
public enum LocalDateTimeToLongConverter implements Converter<LocalDateTime, Long> {
INSTANCE;
@Override
public Long convert(LocalDateTime source) {
return source == null ? null : source.toDate().getTime();
}
}
@WritingConverter
public enum DateTimeToLongConverter implements Converter<DateTime, Long> {
INSTANCE;
@Override
public Long convert(DateTime source) {
return source == null ? null : source.toDate().getTime();
}
}
@WritingConverter
public enum DateMidnightToLongConverter implements Converter<DateMidnight, Long> {
INSTANCE;
@Override
public Long convert(DateMidnight source) {
return source == null ? null : source.toDate().getTime();
}
}
@ReadingConverter
public enum LongToLocalDateConverter implements Converter<Long, LocalDate> {
INSTANCE;
@Override
public LocalDate convert(Long source) {
return source == null ? null : new LocalDate(source);
}
}
@ReadingConverter
public enum LongToLocalDateTimeConverter implements Converter<Long, LocalDateTime> {
INSTANCE;
@Override
public LocalDateTime convert(Long source) {
return source == null ? null : new LocalDateTime(source);
}
}
@ReadingConverter
public enum LongToDateTimeConverter implements Converter<Long, DateTime> {
INSTANCE;
@Override
public DateTime convert(Long source) {
return source == null ? null : new DateTime(source);
}
}
@ReadingConverter
public enum LongToDateMidnightConverter implements Converter<Long, DateMidnight> {
INSTANCE;
@Override
public DateMidnight convert(Long source) {
return source == null ? null : new DateMidnight(source);
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2012-2015 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.data.convert.DefaultTypeMapper;
import org.springframework.data.convert.TypeAliasAccessor;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
/**
* The Couchbase Type Mapper.
*
* @author Michael Nitschinger
*/
public class DefaultCouchbaseTypeMapper extends DefaultTypeMapper<CouchbaseDocument> implements CouchbaseTypeMapper {
/**
* The type key to use if a complex type was identified.
*/
public static final String DEFAULT_TYPE_KEY = "_class";
/**
* Create a new type mapper with the type key.
*
* @param typeKey the typeKey to use.
*/
public DefaultCouchbaseTypeMapper(final String typeKey) {
super(new CouchbaseDocumentTypeAliasAccessor(typeKey));
}
public static final class CouchbaseDocumentTypeAliasAccessor implements TypeAliasAccessor<CouchbaseDocument> {
private final String typeKey;
public CouchbaseDocumentTypeAliasAccessor(final String typeKey) {
this.typeKey = typeKey;
}
@Override
public Object readAliasFrom(final CouchbaseDocument source) {
return source.get(typeKey);
}
@Override
public void writeTypeTo(final CouchbaseDocument sink, final Object alias) {
if (typeKey != null) {
sink.put(typeKey, alias);
}
}
}
}

View File

@@ -0,0 +1,793 @@
/*
* Copyright 2012-2015 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 java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.data.convert.EntityInstantiator;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbaseList;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
import org.springframework.data.mapping.model.PropertyValueProvider;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* A mapping converter for Couchbase.
* <p/>
* The converter is responsible for reading from and writing to entities and converting it into a
* consumable database represenation.
*
* @author Michael Nitschinger
* @author Oliver Gierke
*/
public class MappingCouchbaseConverter extends AbstractCouchbaseConverter
implements ApplicationContextAware {
/**
* The overall application context.
*/
protected ApplicationContext applicationContext;
/**
* The generic mapping context.
*/
protected final MappingContext<? extends CouchbasePersistentEntity<?>,
CouchbasePersistentProperty> mappingContext;
/**
* 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(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() {
return mappingContext;
}
@Override
public <R> R read(final Class<R> clazz, final CouchbaseDocument source) {
return read(ClassTypeInformation.from(clazz), source, 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);
}
/**
* 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;
}
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> entity = (CouchbasePersistentEntity<R>) mappingContext.getPersistentEntity(typeToUse);
if (entity == null) {
throw new MappingException("No mapping metadata found for " + rawType.getName());
}
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);
EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity);
R instance = instantiator.createInstance(entity, provider);
final BeanWrapper<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, result);
wrapper.setProperty(prop, obj);
}
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, result);
wrapper.setProperty(inverseProp, obj);
}
});
return result;
}
/**
* 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);
}
/**
* 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);
return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider,
parent);
}
/**
* 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(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();
Object value = entry.getValue();
TypeInformation<?> keyTypeInformation = type.getComponentType();
if (keyTypeInformation != null) {
Class<?> keyType = keyTypeInformation.getType();
key = conversionService.convert(key, keyType);
}
TypeInformation<?> valueType = type.getMapValueType();
if (value instanceof CouchbaseDocument) {
map.put(key, read(valueType, (CouchbaseDocument) value, parent));
}
else if (value instanceof CouchbaseList) {
map.put(key, readCollection(valueType, (CouchbaseList) value, parent));
}
else {
Class<?> valueClass = valueType == null ? null : valueType.getType();
map.put(key, getPotentiallyConvertedSimpleRead(value, valueClass));
}
}
return map;
}
/**
* 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());
}
if (Class.class.isAssignableFrom(target)) {
try {
return Class.forName(value.toString());
}
catch (ClassNotFoundException e) {
throw new MappingException("Unable to create class from " + value.toString());
}
}
return target.isAssignableFrom(value.getClass()) ? value : conversionService.convert(value, target);
}
@Override
public void write(final Object source, final CouchbaseDocument target) {
if (source == null) {
return;
}
boolean isCustom = conversions.getCustomWriteTarget(source.getClass(), CouchbaseDocument.class) != null;
TypeInformation<?> type = ClassTypeInformation.from(source.getClass());
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.");
}
}
/**
* 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;
}
if (Collection.class.isAssignableFrom(source.getClass())) {
throw new IllegalArgumentException("Root Document must be either CouchbaseDocument or Map.");
}
CouchbasePersistentEntity<?> entity = mappingContext.getPersistentEntity(source.getClass());
writeInternal(source, target, entity);
addCustomTypeKeyIfNecessary(typeHint, source, target);
}
/**
* 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;
}
if (entity == null) {
throw new MappingException("No mapping metadata found for entity of type " + source.getClass().getName());
}
final BeanWrapper<Object> wrapper = BeanWrapper.create(source, conversionService);
final CouchbasePersistentProperty idProperty = entity.getIdProperty();
final CouchbasePersistentProperty versionProperty = entity.getVersionProperty();
if (idProperty != null && target.getId() == null) {
String id = wrapper.getProperty(idProperty, String.class);
target.setId(id);
}
target.setExpiration(entity.getExpiry());
entity.doWithProperties(new PropertyHandler<CouchbasePersistentProperty>() {
@Override
public void doWithPersistentProperty(final CouchbasePersistentProperty prop) {
if (prop.equals(idProperty) || (versionProperty != null && prop.equals(versionProperty))) {
return;
}
Object propertyObj = wrapper.getProperty(prop, prop.getType());
if (null != propertyObj) {
if (!conversions.isSimpleType(propertyObj.getClass())) {
writePropertyInternal(propertyObj, target, prop);
}
else {
writeSimpleInternal(propertyObj, target, prop.getFieldName());
}
}
}
});
entity.doWithAssociations(new AssociationHandler<CouchbasePersistentProperty>() {
@Override
public void doWithAssociation(final Association<CouchbasePersistentProperty> association) {
CouchbasePersistentProperty inverseProp = association.getInverse();
Class<?> type = inverseProp.getType();
Object propertyObj = wrapper.getProperty(inverseProp, type);
if (null != propertyObj) {
writePropertyInternal(propertyObj, target, inverseProp);
}
}
});
}
/**
* 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;
}
String name = prop.getFieldName();
TypeInformation<?> valueType = ClassTypeInformation.from(source.getClass());
TypeInformation<?> type = prop.getTypeInformation();
if (valueType.isCollectionLike()) {
CouchbaseList collectionDoc = createCollection(asCollection(source), prop);
target.put(name, collectionDoc);
return;
}
if (valueType.isMap()) {
CouchbaseDocument mapDoc = createMap((Map<Object, Object>) source, prop);
target.put(name, mapDoc);
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);
writeInternal(source, propertyDoc, entity);
target.put(name, propertyDoc);
}
/**
* 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());
}
/**
* 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();
if (conversions.isSimpleType(key.getClass())) {
String simpleKey = key.toString();
if (val == null || conversions.isSimpleType(val.getClass())) {
writeSimpleInternal(val, target, simpleKey);
}
else if (val instanceof Collection || val.getClass().isArray()) {
target.put(simpleKey, writeCollectionInternal(asCollection(val), new CouchbaseList(conversions.getSimpleTypeHolder()), type.getMapValueType()));
}
else {
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
TypeInformation<?> valueTypeInfo = type.isMap() ? type.getMapValueType() : ClassTypeInformation.OBJECT;
writeInternal(val, embeddedDoc, valueTypeInfo);
target.put(simpleKey, embeddedDoc);
}
}
else {
throw new MappingException("Cannot use a complex object as a key value.");
}
}
return target;
}
/**
* 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(conversions.getSimpleTypeHolder()), prop.getTypeInformation());
}
/**
* 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) {
Class<?> elementType = element == null ? null : element.getClass();
if (elementType == null || conversions.isSimpleType(elementType)) {
target.put(element);
}
else if (element instanceof Collection || elementType.isArray()) {
target.put(writeCollectionInternal(asCollection(element), new CouchbaseList(conversions.getSimpleTypeHolder()), componentType));
}
else {
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
writeInternal(element, embeddedDoc, componentType);
target.put(embeddedDoc);
}
}
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);
Class<?> collectionType = targetType.getType();
if (source.isEmpty()) {
return getPotentiallyConvertedSimpleRead(new HashSet<Object>(), collectionType);
}
collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType : List.class;
Collection<Object> items = targetType.getType().isArray() ? new ArrayList<Object>() : CollectionFactory
.createCollection(collectionType, source.size(false));
TypeInformation<?> componentType = targetType.getComponentType();
Class<?> rawComponentType = componentType == null ? null : componentType.getType();
for (int i = 0; i < source.size(false); i++) {
Object dbObjItem = source.get(i);
if (dbObjItem instanceof CouchbaseDocument) {
items.add(read(componentType, (CouchbaseDocument) dbObjItem, parent));
}
else if (dbObjItem instanceof CouchbaseList) {
items.add(readCollection(componentType, (CouchbaseList) dbObjItem, parent));
}
else {
items.add(getPotentiallyConvertedSimpleRead(dbObjItem, rawComponentType));
}
}
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;
}
return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source);
}
/**
* 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);
}
/**
* 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();
boolean notTheSameClass = !source.getClass().equals(reference);
if (notTheSameClass) {
typeMapper.writeType(source.getClass(), target);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* 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 (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);
}
else {
return (R) getPotentiallyConvertedSimpleRead(value, rawType);
}
}
/**
* 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> {
private final 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(final Object object,
final Parameter<T, CouchbasePersistentProperty> parameter) {
return readValue(object, parameter.getType(), parent);
}
}
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2012-2015 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.translation;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Map;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbaseList;
import org.springframework.data.couchbase.core.mapping.CouchbaseStorable;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.SimpleTypeHolder;
/**
* A Jackson JSON Translator that implements the {@link TranslationService} contract.
*
* @author Michael Nitschinger
*/
public class JacksonTranslationService implements TranslationService, InitializingBean {
/**
* Jackson Object Mapper;
*/
private ObjectMapper objectMapper;
/**
* Type holder to help easily identify simple types.
*/
private SimpleTypeHolder simpleTypeHolder = new SimpleTypeHolder();
/**
* JSON factory for Jackson.
*/
private JsonFactory factory = new JsonFactory();
/**
* Encode a {@link CouchbaseStorable} to a JSON string.
*
* @param source the source document to encode.
* @return the encoded JSON String.
*/
@Override
public final Object encode(final CouchbaseStorable source) {
Writer writer = new StringWriter();
try {
JsonGenerator generator = factory.createGenerator(writer);
encodeRecursive(source, generator);
generator.close();
writer.close();
}
catch (IOException ex) {
throw new RuntimeException("Could not encode JSON", ex);
}
return writer.toString();
}
/**
* Recursively iterates through the sources and adds it to the JSON generator.
*
* @param source the source document
* @param generator the JSON generator.
* @throws IOException
*/
private void encodeRecursive(final CouchbaseStorable source, final JsonGenerator generator) throws IOException {
generator.writeStartObject();
for (Map.Entry<String, Object> entry : ((CouchbaseDocument) source).export().entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
generator.writeFieldName(key);
if (value instanceof CouchbaseDocument) {
encodeRecursive((CouchbaseDocument) value, generator);
continue;
}
final Class<?> clazz = value.getClass();
if (simpleTypeHolder.isSimpleType(clazz) && !isEnumOrClass(clazz)) {
generator.writeObject(value);
}
else {
objectMapper.writeValue(generator, value);
}
}
generator.writeEndObject();
}
private boolean isEnumOrClass(final Class<?> clazz) {
return Enum.class.isAssignableFrom(clazz) || Class.class.isAssignableFrom(clazz);
}
/**
* Decode a JSON string into the {@link CouchbaseStorable} structure.
*
* @param source the source formatted document.
* @param target the target of the populated data.
* @return the decoded structure.
*/
@Override
public final CouchbaseStorable decode(final Object source, final CouchbaseStorable target) {
try {
JsonParser parser = factory.createParser((String) source);
while (parser.nextToken() != null) {
JsonToken currentToken = parser.getCurrentToken();
if (currentToken == JsonToken.START_OBJECT) {
return decodeObject(parser, (CouchbaseDocument) target);
}
else if (currentToken == JsonToken.START_ARRAY) {
return decodeArray(parser, new CouchbaseList());
}
else {
throw new MappingException("JSON to decode needs to start as array or object!");
}
}
parser.close();
}
catch (IOException ex) {
throw new RuntimeException("Could not decode JSON", ex);
}
return target;
}
/**
* Helper method to decode an object recursively.
*
* @param parser the JSON parser with the content.
* @param target the target where the content should be stored.
* @throws IOException
* @returns the decoded object.
*/
private CouchbaseDocument decodeObject(final JsonParser parser, final CouchbaseDocument target) throws IOException {
JsonToken currentToken = parser.nextToken();
String fieldName = "";
while (currentToken != null && currentToken != JsonToken.END_OBJECT) {
if (currentToken == JsonToken.START_OBJECT) {
target.put(fieldName, decodeObject(parser, new CouchbaseDocument()));
}
else if (currentToken == JsonToken.START_ARRAY) {
target.put(fieldName, decodeArray(parser, new CouchbaseList()));
}
else if (currentToken == JsonToken.FIELD_NAME) {
fieldName = parser.getCurrentName();
}
else {
target.put(fieldName, decodePrimitive(currentToken, parser));
}
currentToken = parser.nextToken();
}
return target;
}
/**
* Helper method to decode an array recusrively.
*
* @param parser the JSON parser with the content.
* @param target the target where the content should be stored.
* @throws IOException
* @returns the decoded list.
*/
private CouchbaseList decodeArray(final JsonParser parser, final CouchbaseList target) throws IOException {
JsonToken currentToken = parser.nextToken();
while (currentToken != null && currentToken != JsonToken.END_ARRAY) {
if (currentToken == JsonToken.START_OBJECT) {
target.put(decodeObject(parser, new CouchbaseDocument()));
}
else if (currentToken == JsonToken.START_ARRAY) {
target.put(decodeArray(parser, new CouchbaseList()));
}
else {
target.put(decodePrimitive(currentToken, parser));
}
currentToken = parser.nextToken();
}
return target;
}
/**
* Helper method to decode and assign a primitive.
*
* @param token the type of token.
* @param parser the parser with the content.
* @return the decoded primitve.
* @throws IOException
*/
private Object decodePrimitive(final JsonToken token, final JsonParser parser) throws IOException {
switch (token) {
case VALUE_TRUE:
case VALUE_FALSE:
return parser.getValueAsBoolean();
case VALUE_STRING:
return parser.getValueAsString();
case VALUE_NUMBER_INT:
try {
return parser.getValueAsInt();
}
catch (final JsonParseException e) {
return parser.getValueAsLong();
}
case VALUE_NUMBER_FLOAT:
return parser.getValueAsDouble();
case VALUE_NULL:
return null;
default:
throw new MappingException("Could not decode primitve value " + token);
}
}
public void setObjectMapper(final ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void afterPropertiesSet() {
if (objectMapper == null) {
objectMapper = new ObjectMapper();
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2015 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.translation;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbaseStorable;
/**
* Defines a translation service to encode/decode responses into the {@link CouchbaseStorable} format.
*
* @author Michael Nitschinger
*/
public interface TranslationService {
/**
* Encodes a {@link CouchbaseDocument} into the target format.
*
* @param source the source document to encode.
* @return the encoded document representation.
*/
Object encode(CouchbaseStorable source);
/**
* Decodes the target format into a {@link CouchbaseDocument}
*
* @param source the source formatted document.
* @param target the target of the populated data.
* @return a properly populated document to work with.
*/
CouchbaseStorable decode(Object source, CouchbaseStorable target);
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2012-2015 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.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.support.StandardEvaluationContext;
/**
* The representation of a persistent entity.
*
* @author Michael Nitschinger
*/
public class BasicCouchbasePersistentEntity<T> extends BasicPersistentEntity<T, CouchbasePersistentProperty>
implements CouchbasePersistentEntity<T>, ApplicationContextAware {
/**
* Contains the evaluation context.
*/
private final StandardEvaluationContext context;
/**
* Create a new entity.
*
* @param typeInformation the type information of the entity.
*/
public BasicCouchbasePersistentEntity(final TypeInformation<T> typeInformation) {
super(typeInformation);
context = new StandardEvaluationContext();
}
/**
* Sets the application context.
*
* @param applicationContext the application context.
* @throws BeansException if setting the application context did go wrong.
*/
@Override
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
context.addPropertyAccessor(new BeanFactoryAccessor());
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
context.setRootObject(applicationContext);
}
/**
* Returns the expiration time of the entity.
*
* @return the expiration time.
*/
public int getExpiry() {
org.springframework.data.couchbase.core.mapping.Document annotation =
getType().getAnnotation(org.springframework.data.couchbase.core.mapping.Document.class);
return annotation == null ? 0 : annotation.expiry();
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012-2015 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 java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.util.StringUtils;
/**
* Implements annotated property representations of a given Field instance.
* <p/>
* <p>This object is used to gather information out of properties on objects that need to be persisted. For example, it
* supports overriding of the actual property name by providing custom annotations.</p>
*
* @author Michael Nitschinger
*/
public class BasicCouchbasePersistentProperty
extends AnnotationBasedPersistentProperty<CouchbasePersistentProperty>
implements CouchbasePersistentProperty {
private final FieldNamingStrategy fieldNamingStrategy;
/**
* Create a new instance of the BasicCouchbasePersistentProperty class.
*
* @param field the field of the original reflection.
* @param propertyDescriptor the PropertyDescriptor.
* @param owner the original owner of the property.
* @param simpleTypeHolder the type holder.
*/
public BasicCouchbasePersistentProperty(final Field field, final PropertyDescriptor propertyDescriptor,
final CouchbasePersistentEntity<?> owner, final SimpleTypeHolder simpleTypeHolder,
final FieldNamingStrategy fieldNamingStrategy) {
super(field, propertyDescriptor, owner, simpleTypeHolder);
this.fieldNamingStrategy = fieldNamingStrategy == null ? PropertyNameFieldNamingStrategy.INSTANCE
: fieldNamingStrategy;
}
/**
* Creates a new Association.
*/
@Override
protected Association<CouchbasePersistentProperty> createAssociation() {
return new Association<CouchbasePersistentProperty>(this, null);
}
/**
* Returns the field name of the property.
* <p/>
* The field name can be different from the actual property name by using a
* custom annotation.
*/
@Override
public String getFieldName() {
org.springframework.data.couchbase.core.mapping.Field annotation = getField().
getAnnotation(org.springframework.data.couchbase.core.mapping.Field.class);
if (annotation != null && StringUtils.hasText(annotation.value())) {
return annotation.value();
}
String fieldName = fieldNamingStrategy.getFieldName(this);
if (!StringUtils.hasText(fieldName)) {
throw new MappingException(String.format("Invalid (null or empty) field name returned for property %s by %s!",
this, fieldNamingStrategy.getClass()));
}
return fieldName;
}
}

View File

@@ -0,0 +1,290 @@
/*
* Copyright 2012-2015 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 java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.data.mapping.model.SimpleTypeHolder;
/**
* A {@link CouchbaseDocument} is an abstract representation of a document stored inside Couchbase Server.
* <p/>
* <p>It acts like a {@link HashMap}, but only allows those types to be written that are supported by the underlying
* storage format, which is currently JSON. Note that JSON conversion is not happening here, but performed at a
* different stage based on the payload stored in the {@link CouchbaseDocument}.</p>
* <p/>
* <p>In addition to the actual content, meta data is also stored. This especially refers to the document ID and its
* expiration time. Note that this information is not mandatory, since documents can be nested and therefore only the
* topmost document most likely has an ID.</p>
*
* @author Michael Nitschinger
*/
public class CouchbaseDocument implements CouchbaseStorable {
/**
* Defnes the default expiration time for the document.
*/
public static final int DEFAULT_EXPIRATION_TIME = 0;
/**
* Contains the actual data to be stored.
*/
private HashMap<String, Object> payload;
/**
* Represents the document ID used to identify the document in the bucket.
*/
private String id;
/**
* Contains the expiration time of the document.
*/
private int expiration;
/**
* Holds types considered simple and allowed to be stored.
*/
private SimpleTypeHolder simpleTypeHolder;
/**
* Creates a completely empty {@link CouchbaseDocument}.
*/
public CouchbaseDocument() {
this(null);
}
/**
* Creates a empty {@link CouchbaseDocument}, and set the ID immediately.
*
* @param id the document ID.
*/
public CouchbaseDocument(final String id) {
this(id, DEFAULT_EXPIRATION_TIME);
}
/**
* Creates a empty {@link CouchbaseDocument} with ID and expiration time.
*
* @param id the document ID.
* @param expiration the expiration time of the document.
*/
public CouchbaseDocument(final String id, final int expiration) {
this.id = id;
this.expiration = expiration;
payload = new HashMap<String, Object>();
Set<Class<?>> additionalTypes = new HashSet<Class<?>>();
additionalTypes.add(CouchbaseDocument.class);
additionalTypes.add(CouchbaseList.class);
simpleTypeHolder = new SimpleTypeHolder(additionalTypes, true);
}
/**
* Store a value with the given key for later retreival.
*
* @param key the key of the attribute.
* @param value the actual content to be stored.
* @return the {@link CouchbaseDocument} for chaining.
*/
public final CouchbaseDocument put(final String key, final Object value) {
verifyValueType(value);
payload.put(key, value);
return this;
}
/**
* Potentially get a value from the payload with the given key.
*
* @param key the key of the attribute.
* @return the value to which the specified key is mapped, or
* null if does not contain a mapping for the key.
*/
public final Object get(final String key) {
return payload.get(key);
}
/**
* Returns the current payload, including all recursive elements.
* <p/>
* It either returns the raw results or makes sure that the recusrive elements
* are also exported properly.
*
* @return
*/
public final HashMap<String, Object> export() {
HashMap<String, Object> toExport = new HashMap<String, Object>(payload);
for (Map.Entry<String, Object> entry : payload.entrySet()) {
if (entry.getValue() instanceof CouchbaseDocument) {
toExport.put(entry.getKey(), ((CouchbaseDocument) entry.getValue()).export());
}
else if (entry.getValue() instanceof CouchbaseList) {
toExport.put(entry.getKey(), ((CouchbaseList) entry.getValue()).export());
}
}
return toExport;
}
/**
* Returns true if it contains a payload for the specified key.
*
* @param key the key of the attribute.
* @return true if it contains a payload for the specified key.
*/
public final boolean containsKey(final String key) {
return payload.containsKey(key);
}
/**
* Returns true if it contains the given value.
*
* @param value the value to check for.
* @return true if it contains the specified value.
*/
public final boolean containsValue(final Object value) {
return payload.containsValue(value);
}
/**
* Returns the size of the attributes in this document (not nested).
*
* @return the size of the attributes in this document (not nested).
*/
public final int size() {
return size(false);
}
/**
* Retruns the size of the attributes in this and recursive documents.
*
* @param recursive wheter nested attributes should be taken into account.
* @return the size of the attributes in this and recursive documents.
*/
public final int size(final boolean recursive) {
int thisSize = payload.size();
if (!recursive || thisSize == 0) {
return thisSize;
}
int totalSize = thisSize;
for (Object value : payload.values()) {
if (value instanceof CouchbaseDocument) {
totalSize += ((CouchbaseDocument) value).size(true);
}
else if (value instanceof CouchbaseList) {
totalSize += ((CouchbaseList) value).size(true);
}
}
return totalSize;
}
/**
* Returns the underlying payload.
* <p/>
* <p>Note that unlike {@link #export()}, the nested objects are not converted, so the "raw" map is returned.</p>
*
* @return the underlying payload.
*/
public HashMap<String, Object> getPayload() {
return payload;
}
/**
* Returns the expiration time of the document.
* <p/>
* If the expiration time is 0, then the document will be persisted until
* deleted manually ("forever").
*
* @return the expiration time of the document.
*/
public int getExpiration() {
return expiration;
}
/**
* Set the expiration time of the document.
* <p/>
* If the expiration time is 0, then the document will be persisted until
* deleted manually ("forever").
*
* @param expiration
* @return the {@link CouchbaseDocument} for chaining.
*/
public CouchbaseDocument setExpiration(int expiration) {
this.expiration = expiration;
return this;
}
/**
* Returns the ID of the document.
*
* @return the ID of the document.
*/
public String getId() {
return id;
}
/**
* Sets the unique ID of the document per bucket.
*
* @param id the ID of the document.
* @return the {@link CouchbaseDocument} for chaining.
*/
public CouchbaseDocument setId(String id) {
this.id = id;
return this;
}
/**
* Verifies that only values of a certain and supported type
* can be stored.
* <p/>
* <p>If this is not the case, a {@link IllegalArgumentException} is
* thrown.</p>
*
* @param value the object to verify its type.
*/
private void verifyValueType(final Object value) {
if (value == null) {
return;
}
final Class<?> clazz = value.getClass();
if (simpleTypeHolder.isSimpleType(clazz)) {
return;
}
throw new IllegalArgumentException("Attribute of type " + clazz.getCanonicalName() + " cannot be stored and must be converted.");
}
/**
* A string representation of expiration, id and payload.
*
* @return the string representation of the object.
*/
@Override
public String toString() {
return "CouchbaseDocument{" +
"id=" + id +
", exp=" + expiration +
", payload=" + payload +
'}';
}
}

View File

@@ -0,0 +1,225 @@
/*
* Copyright 2012-2015 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 java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.data.mapping.model.SimpleTypeHolder;
/**
* A {@link CouchbaseList} is an abstract list that represents an array stored in a (most of the times JSON) document.
* <p/>
* <p>This {@link CouchbaseList} is part of the potentially nested structure inside one or more
* {@link CouchbaseDocument}s. It can also contain them recursively, depending on how the document is modeled.</p>
*/
public class CouchbaseList implements CouchbaseStorable {
/**
* Contains the actual data to be stored.
*/
private List<Object> payload;
/**
* Holds types considered simple and allowed to be stored.
*/
private SimpleTypeHolder simpleTypeHolder;
/**
* Create a new (empty) list.
*/
public CouchbaseList() {
this(new ArrayList<Object>());
}
/**
* Create a new list with a given payload on construction.
*
* @param initialPayload the initial data to store.
*/
public CouchbaseList(final List<Object> initialPayload) {
this(initialPayload, null);
}
/**
* Create a new (empty) list with an existing {@link SimpleTypeHolder}.
*
* @param simpleTypeHolder context instance.
*/
public CouchbaseList(final SimpleTypeHolder simpleTypeHolder) {
this(new ArrayList<Object>(), simpleTypeHolder);
}
/**
* Create a new list with a given payload on construction and an existing {@link SimpleTypeHolder}.
*
* @param initialPayload the initial data to store.
* @param simpleTypeHolder context instance.
*/
public CouchbaseList(final List<Object> initialPayload, final SimpleTypeHolder simpleTypeHolder) {
this.payload = initialPayload;
Set<Class<?>> additionalTypes = new HashSet<Class<?>>();
additionalTypes.add(CouchbaseDocument.class);
additionalTypes.add(CouchbaseList.class);
if (simpleTypeHolder != null) {
this.simpleTypeHolder = new SimpleTypeHolder(additionalTypes, simpleTypeHolder);
}
else {
this.simpleTypeHolder = new SimpleTypeHolder(additionalTypes, true);
}
}
/**
* Add content to the underlying list.
*
* @param value the value to be added.
* @return the {@link CouchbaseList} object for chaining purposes.
*/
public final CouchbaseList put(final Object value) {
verifyValueType(value);
payload.add(value);
return this;
}
/**
* Return the stored element at the given index.
*
* @param index the index where the document is located.
* @return the found object (or null if nothing found).
*/
public final Object get(final int index) {
return payload.get(index);
}
/**
* Returns the size of the attributes in this document (not nested).
*
* @return the size of the attributes in this document (not nested).
*/
public final int size() {
return size(false);
}
/**
* Retruns the size of the attributes in this and recursive documents.
*
* @param recursive wheter nested attributes should be taken into account.
* @return the size of the attributes in this and recursive documents.
*/
public final int size(final boolean recursive) {
int thisSize = payload.size();
if (!recursive || thisSize == 0) {
return thisSize;
}
int totalSize = thisSize;
for (Object value : payload) {
if (value instanceof CouchbaseDocument) {
totalSize += ((CouchbaseDocument) value).size(true);
}
else if (value instanceof CouchbaseList) {
totalSize += ((CouchbaseList) value).size(true);
}
}
return totalSize;
}
/**
* Returns the current payload, including all recursive elements.
* <p/>
* It either returns the raw results or makes sure that the recusrive elements
* are also exported properly.
*
* @return
*/
public final List<Object> export() {
List<Object> toExport = new ArrayList<Object>(payload);
int elem = 0;
for (Object entry : payload) {
if (entry instanceof CouchbaseDocument) {
toExport.remove(elem);
toExport.add(elem, ((CouchbaseDocument) entry).export());
}
else if (entry instanceof CouchbaseList) {
toExport.remove(elem);
toExport.add(elem, ((CouchbaseList) entry).export());
}
elem++;
}
return toExport;
}
/**
* Returns true if it contains the given value.
*
* @param value the value to check for.
* @return true if it contains the specified value.
*/
public final boolean containsValue(final Object value) {
return payload.contains(value);
}
/**
* Checks if the underlying payload is empty or not.
*
* @return whether the underlying payload is empty or not.
*/
public final boolean isEmpty() {
return payload.isEmpty();
}
/**
* Verifies that only values of a certain and supported type
* can be stored.
* <p/>
* <p>If this is not the case, a {@link IllegalArgumentException} is
* thrown.</p>
*
* @param value the object to verify its type.
*/
private void verifyValueType(final Object value) {
if (value == null) {
return;
}
final Class<?> clazz = value.getClass();
if (simpleTypeHolder.isSimpleType(clazz)) {
return;
}
throw new IllegalArgumentException("Attribute of type "
+ clazz.getCanonicalName() + "can not be stored and must be converted.");
}
/**
* A string reprensation of the payload.
*
* @return the underlying payload as a string representation for easier debugging.
*/
@Override
public String toString() {
return "CouchbaseList{" +
"payload=" + payload +
'}';
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2012-2015 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 java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.mapping.context.AbstractMappingContext;
import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.TypeInformation;
/**
* Default implementation of a {@link org.springframework.data.mapping.context.MappingContext} for Couchbase using
* {@link BasicCouchbasePersistentEntity} and {@link BasicCouchbasePersistentProperty} as primary abstractions.
*
* @author Michael Nitschinger
*/
public class CouchbaseMappingContext
extends AbstractMappingContext<BasicCouchbasePersistentEntity<?>, CouchbasePersistentProperty>
implements ApplicationContextAware {
/**
* Contains the application context to configure the application.
*/
private ApplicationContext context;
/**
* The default field naming strategy.
*/
private static final FieldNamingStrategy DEFAULT_NAMING_STRATEGY = PropertyNameFieldNamingStrategy.INSTANCE;
/**
* The field naming strategy to use.
*/
private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
/**
* Configures the {@link FieldNamingStrategy} to be used to determine the field name if no manual mapping is applied.
* Defaults to a strategy using the plain property name.
*
* @param fieldNamingStrategy the {@link FieldNamingStrategy} to be used to determine the field name if no manual
* mapping is applied.
*/
public void setFieldNamingStrategy(final FieldNamingStrategy fieldNamingStrategy) {
this.fieldNamingStrategy = fieldNamingStrategy == null ? DEFAULT_NAMING_STRATEGY : fieldNamingStrategy;
}
/**
* Creates a concrete entity based out of the type information passed.
*
* @param typeInformation type information of the entity to create.
* @param <T> the type for the corresponding type information.
* @return the constructed entity.
*/
@Override
protected <T> BasicCouchbasePersistentEntity<?> createPersistentEntity(final TypeInformation<T> typeInformation) {
BasicCouchbasePersistentEntity<T> entity = new BasicCouchbasePersistentEntity<T>(typeInformation);
if (context != null) {
entity.setApplicationContext(context);
}
return entity;
}
/**
* Creates a concrete property based on the field information and entity.
*
* @param field the reflection on the field to be used as a property.
* @param descriptor the property descriptor.
* @param owner the entity which owns the property.
* @param simpleTypeHolder the type holder.
* @return the constructed property.
*/
@Override
protected CouchbasePersistentProperty createPersistentProperty(final Field field, final PropertyDescriptor descriptor,
final BasicCouchbasePersistentEntity<?> owner, final SimpleTypeHolder simpleTypeHolder) {
return new BasicCouchbasePersistentProperty(field, descriptor, owner, simpleTypeHolder, fieldNamingStrategy);
}
/**
* Sets (or overrides) the current application context.
*
* @param applicationContext the application context to be assigned.
* @throws BeansException if the context can not be set properly.
*/
@Override
public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2012-2015 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.springframework.data.mapping.PersistentEntity;
/**
* Represents an entity that can be persisted which contains 0 or more properties.
*
* @author Michael Nitschinger
*/
public interface CouchbasePersistentEntity<T> extends
PersistentEntity<T, CouchbasePersistentProperty> {
/**
* Returns the expiry time for the document.
*
* @return the expiration time.
*/
int getExpiry();
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2015 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.springframework.data.mapping.PersistentProperty;
/**
* Represents a property part of an entity that needs to be persisted.
*
* @author Michael Nitschinger
*/
public interface CouchbasePersistentProperty extends PersistentProperty<CouchbasePersistentProperty> {
/**
* Returns the field name of the property.
* <p/>
* The field name can be different from the actual property name by using a custom annotation.
*/
String getFieldName();
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2015 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 java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.mapping.model.SimpleTypeHolder;
public abstract class CouchbaseSimpleTypes {
static {
Set<Class<?>> simpleTypes = new HashSet<Class<?>>();
simpleTypes.add(CouchbaseDocument.class);
simpleTypes.add(CouchbaseList.class);
COUCHBASE_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes);
}
private static final Set<Class<?>> COUCHBASE_SIMPLE_TYPES;
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(COUCHBASE_SIMPLE_TYPES, true);
private CouchbaseSimpleTypes() {
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2012-2015 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;
/**
* Marker Interface to identify either a {@link CouchbaseDocument} or a {@link CouchbaseList}.
* <p/>
* This interface will be extended in the future to refactor the needed infrastructure into the common interface.
*
* @author Michael Nitschinger
*/
public interface CouchbaseStorable {
}