From 56cf714ef88cceb580ce0f5c365f4145920cf173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 22 Jun 2015 19:52:45 +0200 Subject: [PATCH] added back the convert and mapping packages from 1.x --- .../convert/AbstractCouchbaseConverter.java | 91 ++ .../core/convert/ConverterRegistration.java | 115 +++ .../core/convert/CouchbaseConverter.java | 35 + .../CouchbaseDocumentPropertyAccessor.java | 76 ++ .../core/convert/CouchbaseTypeMapper.java | 29 + .../core/convert/CouchbaseWriter.java | 28 + .../core/convert/CustomConversions.java | 340 ++++++++ .../core/convert/DateConverters.java | 206 +++++ .../convert/DefaultCouchbaseTypeMapper.java | 65 ++ .../convert/MappingCouchbaseConverter.java | 793 ++++++++++++++++++ .../JacksonTranslationService.java | 254 ++++++ .../translation/TranslationService.java | 45 + .../BasicCouchbasePersistentEntity.java | 75 ++ .../BasicCouchbasePersistentProperty.java | 93 ++ .../core/mapping/CouchbaseDocument.java | 290 +++++++ .../couchbase/core/mapping/CouchbaseList.java | 225 +++++ .../core/mapping/CouchbaseMappingContext.java | 109 +++ .../mapping/CouchbasePersistentEntity.java | 36 + .../mapping/CouchbasePersistentProperty.java | 35 + .../core/mapping/CouchbaseSimpleTypes.java | 41 + .../core/mapping/CouchbaseStorable.java | 27 + 21 files changed, 3008 insertions(+) create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseDocumentPropertyAccessor.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseWriter.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/DateConverters.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseList.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseStorable.java diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java new file mode 100644 index 00000000..4040f096 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java @@ -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); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java new file mode 100644 index 00000000..4a8b53c0 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java @@ -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); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java new file mode 100644 index 00000000..fbdf0508 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java @@ -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, + CouchbasePersistentProperty, Object, CouchbaseDocument>, + CouchbaseWriter, + EntityReader { +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseDocumentPropertyAccessor.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseDocumentPropertyAccessor.java new file mode 100644 index 00000000..74812886 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseDocumentPropertyAccessor.java @@ -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 source = (Map) target; + + Object value = source.get(name); + return value == null ? TypedValue.NULL : new TypedValue(value); + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java new file mode 100644 index 00000000..93615e11 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java @@ -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 { + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseWriter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseWriter.java new file mode 100644 index 00000000..914a20c5 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseWriter.java @@ -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 extends + EntityWriter { +} \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java b/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java new file mode 100644 index 00000000..fadcc384 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java @@ -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. + *

+ *

Types that can be mapped directly onto JSON are considered simple ones, because they neither need deeper + * inspection nor nested conversion.

+ * + * @author Michael Nitschinger + * @author Oliver Gierke + */ +public class CustomConversions { + + private static final Logger LOG = LoggerFactory.getLogger(CustomConversions.class); + private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a Couchbase supported type! You might wanna check you annotation setup at the converter implementation."; + private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a Couchbase supported type! You might wanna check you annotation setup at the converter implementation."; + + /** + * Contains the simple type holder. + */ + private final SimpleTypeHolder simpleTypeHolder; + + private final List converters; + + private final Set readingPairs; + private final Set writingPairs; + private final Set> customSimpleTypes; + private final ConcurrentMap customReadTargetTypes; + + /** + * Create a new instance with no converters. + */ + CustomConversions() { + this(new ArrayList()); + } + + /** + * 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(); + writingPairs = new LinkedHashSet(); + customSimpleTypes = new HashSet>(); + customReadTargetTypes = new ConcurrentHashMap(); + + this.converters = new ArrayList(); + 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 pairs) { + Assert.notNull(sourceType); + Assert.notNull(pairs); + + for (GenericConverter.ConvertiblePair typePair : pairs) { + if (typePair.getSourceType().isAssignableFrom(sourceType)) { + Class targetType = typePair.getTargetType(); + if (requestedTargetType == null || targetType.isAssignableFrom(requestedTargetType)) { + return targetType; + } + } + } + + return null; + } + + /** + * Wrapper to safely store {@literal null} values in the type cache. + * + * @author Patryk Wasik + * @author Oliver Gierke + * @author Thomas Darimont + */ + private static class CacheValue { + + private static final CacheValue ABSENT = new CacheValue(null); + + private final Class type; + + public CacheValue(Class type) { + this.type = type; + } + + public Class getType() { + return type; + } + + static CacheValue of(Class type) { + return type == null ? ABSENT : new CacheValue(type); + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/DateConverters.java b/src/main/java/org/springframework/data/couchbase/core/convert/DateConverters.java new file mode 100644 index 00000000..e3f5da77 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/DateConverters.java @@ -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> getConvertersToRegister() { + List> converters = new ArrayList>(); + + 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 { + INSTANCE; + + @Override + public Long convert(Date source) { + return source == null ? null : source.getTime(); + } + } + + @WritingConverter + public enum CalendarToLongConverter implements Converter { + INSTANCE; + + @Override + public Long convert(Calendar source) { + return source == null ? null : source.getTimeInMillis() / 1000; + } + } + + @ReadingConverter + public enum LongToDateConverter implements Converter { + 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 { + 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 { + INSTANCE; + + @Override + public Long convert(LocalDate source) { + return source == null ? null : source.toDate().getTime(); + } + } + + @WritingConverter + public enum LocalDateTimeToLongConverter implements Converter { + INSTANCE; + + @Override + public Long convert(LocalDateTime source) { + return source == null ? null : source.toDate().getTime(); + } + } + + @WritingConverter + public enum DateTimeToLongConverter implements Converter { + INSTANCE; + + @Override + public Long convert(DateTime source) { + return source == null ? null : source.toDate().getTime(); + } + } + + @WritingConverter + public enum DateMidnightToLongConverter implements Converter { + INSTANCE; + + @Override + public Long convert(DateMidnight source) { + return source == null ? null : source.toDate().getTime(); + } + } + + @ReadingConverter + public enum LongToLocalDateConverter implements Converter { + INSTANCE; + + @Override + public LocalDate convert(Long source) { + return source == null ? null : new LocalDate(source); + } + } + + @ReadingConverter + public enum LongToLocalDateTimeConverter implements Converter { + INSTANCE; + + @Override + public LocalDateTime convert(Long source) { + return source == null ? null : new LocalDateTime(source); + } + } + + @ReadingConverter + public enum LongToDateTimeConverter implements Converter { + INSTANCE; + + @Override + public DateTime convert(Long source) { + return source == null ? null : new DateTime(source); + } + } + + @ReadingConverter + public enum LongToDateMidnightConverter implements Converter { + INSTANCE; + + @Override + public DateMidnight convert(Long source) { + return source == null ? null : new DateMidnight(source); + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java b/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java new file mode 100644 index 00000000..a82a96f9 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java @@ -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 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 { + + 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); + } + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java new file mode 100644 index 00000000..3791382c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -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. + *

+ * 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, + 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, + CouchbasePersistentProperty> mappingContext) { + super(ConversionServiceFactory.createDefaultConversionService()); + + this.mappingContext = mappingContext; + typeMapper = new DefaultCouchbaseTypeMapper(DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY); + spELContext = new SpELContext(CouchbaseDocumentPropertyAccessor.INSTANCE); + } + + @Override + public MappingContext, CouchbasePersistentProperty> getMappingContext() { + return mappingContext; + } + + @Override + public R read(final Class 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 the entity type. + * @return the converted entity. + */ + protected R read(final TypeInformation 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 the entity type. + * @return the converted entity. + */ + @SuppressWarnings("unchecked") + protected R read(final TypeInformation type, final CouchbaseDocument source, final Object parent) { + if (source == null) { + return null; + } + + TypeInformation typeToUse = typeMapper.readType(source, type); + Class rawType = typeToUse.getType(); + + if (conversions.hasCustomReadTarget(source.getClass(), rawType)) { + return conversionService.convert(source, rawType); + } + + if (typeToUse.isMap()) { + return (R) readMap(typeToUse, source, parent); + } + + CouchbasePersistentEntity entity = (CouchbasePersistentEntity) 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 the entity type. + * @return the converted entity. + */ + protected R read(final CouchbasePersistentEntity entity, final CouchbaseDocument source, final Object parent) { + final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(source, spELContext); + ParameterValueProvider provider = + getParameterProvider(entity, source, evaluator, parent); + EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); + + R instance = instantiator.createInstance(entity, provider); + final BeanWrapper wrapper = BeanWrapper.create(instance, conversionService); + final R result = wrapper.getBean(); + + entity.doWithProperties(new PropertyHandler() { + @Override + public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { + if (!doesPropertyExistInSource(prop) || entity.isConstructorArgument(prop)) { + return; + } + Object obj = prop.isIdProperty() ? source.getId() : getValueInternal(prop, source, result); + wrapper.setProperty(prop, obj); + } + + private boolean doesPropertyExistInSource(final CouchbasePersistentProperty property) { + return property.isIdProperty() || source.containsKey(property.getFieldName()); + } + }); + + entity.doWithAssociations(new AssociationHandler() { + @Override + public void doWithAssociation(final Association association) { + CouchbasePersistentProperty inverseProp = association.getInverse(); + Object obj = getValueInternal(inverseProp, source, 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 getParameterProvider( + final CouchbasePersistentEntity entity, final CouchbaseDocument source, + final DefaultSpELExpressionEvaluator evaluator, final Object parent) { + CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, evaluator, parent); + PersistentEntityParameterValueProvider parameterProvider = + new PersistentEntityParameterValueProvider(entity, provider, parent); + + 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 readMap(final TypeInformation type, final CouchbaseDocument source, + final Object parent) { + Assert.notNull(source); + + Class mapType = typeMapper.readType(source, type).getType(); + Map map = CollectionFactory.createMap(mapType, source.export().keySet().size()); + Map sourceMap = source.getPayload(); + + for (Map.Entry entry : sourceMap.entrySet()) { + Object key = entry.getKey(); + 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) 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) 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 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 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() { + @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() { + @Override + public void doWithAssociation(final Association 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) 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 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 source, final CouchbaseDocument target, + final TypeInformation type) { + for (Map.Entry 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(), collectionType); + } + + collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType : List.class; + Collection items = targetType.getType().isArray() ? new ArrayList() : 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 the target type. + * @return the converted object. + */ + @SuppressWarnings("unchecked") + private 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 { + + /** + * The source document. + */ + private final CouchbaseDocument source; + + /** + * The expression evaluator. + */ + private final SpELExpressionEvaluator evaluator; + + /** + * The optional parent object. + */ + private final Object parent; + + public CouchbasePropertyValueProvider(final CouchbaseDocument source, final SpELContext factory, + final Object parent) { + this(source, new DefaultSpELExpressionEvaluator(source, factory), parent); + } + + public CouchbasePropertyValueProvider(final CouchbaseDocument source, + final DefaultSpELExpressionEvaluator evaluator, final Object parent) { + Assert.notNull(source); + Assert.notNull(evaluator); + + this.source = source; + this.evaluator = evaluator; + this.parent = parent; + } + + @Override + @SuppressWarnings("unchecked") + public R getPropertyValue(final CouchbasePersistentProperty property) { + String expression = property.getSpelExpression(); + Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName()); + + if (property.isIdProperty()) { + return (R) source.getId(); + } + if (value == null) { + return null; + } + + return readValue(value, property.getTypeInformation(), parent); + } + } + + /** + * A expression parameter value provider. + */ + private class ConverterAwareSpELExpressionParameterValueProvider extends + SpELExpressionParameterValueProvider { + + private final Object parent; + + public ConverterAwareSpELExpressionParameterValueProvider(final SpELExpressionEvaluator evaluator, + final ConversionService conversionService, final ParameterValueProvider delegate, + final Object parent) { + super(evaluator, conversionService, delegate); + this.parent = parent; + } + + @Override + protected T potentiallyConvertSpelValue(final Object object, + final Parameter parameter) { + return readValue(object, parameter.getType(), parent); + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java new file mode 100644 index 00000000..e87d2f59 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java @@ -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 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(); + } + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java new file mode 100644 index 00000000..89940054 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java @@ -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); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java new file mode 100644 index 00000000..522e4db6 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java @@ -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 extends BasicPersistentEntity + implements CouchbasePersistentEntity, 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 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(); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java new file mode 100644 index 00000000..0f135cc8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java @@ -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. + *

+ *

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.

+ * + * @author Michael Nitschinger + */ +public class BasicCouchbasePersistentProperty + extends AnnotationBasedPersistentProperty + 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 createAssociation() { + return new Association(this, null); + } + + /** + * Returns the field name of the property. + *

+ * 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; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java new file mode 100644 index 00000000..e6cc3592 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java @@ -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. + *

+ *

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}.

+ *

+ *

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.

+ * + * @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 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(); + + Set> additionalTypes = new HashSet>(); + 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. + *

+ * It either returns the raw results or makes sure that the recusrive elements + * are also exported properly. + * + * @return + */ + public final HashMap export() { + HashMap toExport = new HashMap(payload); + for (Map.Entry 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. + *

+ *

Note that unlike {@link #export()}, the nested objects are not converted, so the "raw" map is returned.

+ * + * @return the underlying payload. + */ + public HashMap getPayload() { + return payload; + } + + /** + * Returns the expiration time of the document. + *

+ * 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. + *

+ * 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. + *

+ *

If this is not the case, a {@link IllegalArgumentException} is + * thrown.

+ * + * @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 + + '}'; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseList.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseList.java new file mode 100644 index 00000000..7429080c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseList.java @@ -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. + *

+ *

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.

+ */ +public class CouchbaseList implements CouchbaseStorable { + + /** + * Contains the actual data to be stored. + */ + private List payload; + + /** + * Holds types considered simple and allowed to be stored. + */ + private SimpleTypeHolder simpleTypeHolder; + + /** + * Create a new (empty) list. + */ + public CouchbaseList() { + this(new ArrayList()); + } + + /** + * Create a new list with a given payload on construction. + * + * @param initialPayload the initial data to store. + */ + public CouchbaseList(final List 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(), 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 initialPayload, final SimpleTypeHolder simpleTypeHolder) { + this.payload = initialPayload; + Set> additionalTypes = new HashSet>(); + 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. + *

+ * It either returns the raw results or makes sure that the recusrive elements + * are also exported properly. + * + * @return + */ + public final List export() { + List toExport = new ArrayList(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. + *

+ *

If this is not the case, a {@link IllegalArgumentException} is + * thrown.

+ * + * @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 + + '}'; + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java new file mode 100644 index 00000000..f9b86f95 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java @@ -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, 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 the type for the corresponding type information. + * @return the constructed entity. + */ + @Override + protected BasicCouchbasePersistentEntity createPersistentEntity(final TypeInformation typeInformation) { + BasicCouchbasePersistentEntity entity = new BasicCouchbasePersistentEntity(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; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java new file mode 100644 index 00000000..5c5d3c6f --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java @@ -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 extends + PersistentEntity { + + /** + * Returns the expiry time for the document. + * + * @return the expiration time. + */ + int getExpiry(); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java new file mode 100644 index 00000000..b7553b08 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentProperty.java @@ -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 { + + /** + * Returns the field name of the property. + *

+ * The field name can be different from the actual property name by using a custom annotation. + */ + String getFieldName(); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java new file mode 100644 index 00000000..559d8e64 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java @@ -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> simpleTypes = new HashSet>(); + simpleTypes.add(CouchbaseDocument.class); + simpleTypes.add(CouchbaseList.class); + COUCHBASE_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); + } + + private static final Set> COUCHBASE_SIMPLE_TYPES; + public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(COUCHBASE_SIMPLE_TYPES, true); + + private CouchbaseSimpleTypes() { + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseStorable.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseStorable.java new file mode 100644 index 00000000..088b1bb9 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseStorable.java @@ -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}. + *

+ * This interface will be extended in the future to refactor the needed infrastructure into the common interface. + * + * @author Michael Nitschinger + */ +public interface CouchbaseStorable { +}