diff --git a/README.md b/README.md index 5595d742..cb8a86c0 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,14 @@ Full documentation is still in the making, so this README outlines the basic ste - Templates - JavaConfig + - Mapping of arbitrary Objects (Value Objects) + - View support in template - CRUD Repository (aside *All and count methods, see planned) - Basic Auditing (JMX) - Additional: transparent @Cacheable support ### Planned (before 1.0) - - Mapping of arbitrary Objects (Value Objects) - - View support in template - XML Config (namespace for template + repositories) - find*-based methods on repositories through Views - @View annotation for customization diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java index 6609cbf6..ca13cfbc 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -26,9 +26,12 @@ import net.spy.memcached.internal.OperationFuture; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; -import org.springframework.data.couchbase.core.mapping.ConvertedCouchbaseDocument; +import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.CouchbaseStorable; import org.springframework.data.mapping.context.MappingContext; import com.couchbase.client.CouchbaseClient; @@ -45,6 +48,7 @@ public class CouchbaseTemplate implements CouchbaseOperations { private static final Collection ITERABLE_CLASSES; private final CouchbaseExceptionTranslator exceptionTranslator = new CouchbaseExceptionTranslator(); + private final TranslationService translationService; static { Set iterableClasses = new HashSet(); @@ -61,8 +65,9 @@ public class CouchbaseTemplate implements CouchbaseOperations { public CouchbaseTemplate(final CouchbaseClient client, final CouchbaseConverter converter) { this.client = client; - this.couchbaseConverter = converter == null ? getDefaultConverter(client) : converter; - this.mappingContext = this.couchbaseConverter.getMappingContext(); + couchbaseConverter = converter == null ? getDefaultConverter(client) : converter; + mappingContext = couchbaseConverter.getMappingContext(); + translationService = new JacksonTranslationService(); } private CouchbaseConverter getDefaultConverter(final CouchbaseClient client) { @@ -72,24 +77,32 @@ public class CouchbaseTemplate implements CouchbaseOperations { return converter; } + private Object translateEncode(final CouchbaseStorable source) { + return translationService.encode(source); + } + + private CouchbaseStorable translateDecode(final String source, final CouchbaseStorable target) { + return translationService.decode(source, target); + } + public final void insert(final Object objectToSave) { ensureNotIterable(objectToSave); - final ConvertedCouchbaseDocument converted = - new ConvertedCouchbaseDocument(); + final CouchbaseDocument converted = new CouchbaseDocument(); couchbaseConverter.write(objectToSave, converted); + execute(new BucketCallback>() { @Override public OperationFuture doInBucket() { return client.add( - converted.getId(), converted.getExpiry(), converted.getRawValue()); + converted.getId(), converted.getExpiration(), translateEncode(converted)); } }); } public final void insert(final Collection batchToSave) { Iterator iter = batchToSave.iterator(); - while(iter.hasNext()) { + while (iter.hasNext()) { insert(iter.next()); } } @@ -97,15 +110,14 @@ public class CouchbaseTemplate implements CouchbaseOperations { public void save(final Object objectToSave) { ensureNotIterable(objectToSave); - final ConvertedCouchbaseDocument converted = - new ConvertedCouchbaseDocument(); + final CouchbaseDocument converted = new CouchbaseDocument(); couchbaseConverter.write(objectToSave, converted); execute(new BucketCallback>() { @Override public OperationFuture doInBucket() { return client.set( - converted.getId(), converted.getExpiry(), converted.getRawValue()); + converted.getId(), converted.getExpiration(), translateEncode(converted)); } }); } @@ -120,15 +132,14 @@ public class CouchbaseTemplate implements CouchbaseOperations { public void update(final Object objectToSave) { ensureNotIterable(objectToSave); - final ConvertedCouchbaseDocument converted = - new ConvertedCouchbaseDocument(); + final CouchbaseDocument converted = new CouchbaseDocument(); couchbaseConverter.write(objectToSave, converted); execute(new BucketCallback>() { @Override public OperationFuture doInBucket() { return client.replace( - converted.getId(), converted.getExpiry(), converted.getRawValue()); + converted.getId(), converted.getExpiration(), translateEncode(converted)); } }); @@ -152,9 +163,9 @@ public class CouchbaseTemplate implements CouchbaseOperations { if (result == null) { return null; } - - ConvertedCouchbaseDocument converted = new ConvertedCouchbaseDocument(id, result); - return couchbaseConverter.read(entityClass, converted); + + CouchbaseDocument converted = new CouchbaseDocument(id); + return couchbaseConverter.read(entityClass, (CouchbaseDocument) translateDecode(result, converted)); } @@ -173,9 +184,9 @@ public class CouchbaseTemplate implements CouchbaseOperations { List result = new ArrayList(response.size()); for (ViewRow row : response) { - ConvertedCouchbaseDocument converted = - new ConvertedCouchbaseDocument(row.getId(), (String) row.getDocument()); - result.add(couchbaseConverter.read(entityClass, converted)); + CouchbaseDocument converted = new CouchbaseDocument(row.getId()); + result.add(couchbaseConverter.read(entityClass, + (CouchbaseDocument) translateDecode((String) row.getDocument(), converted))); } return result; @@ -206,7 +217,7 @@ public class CouchbaseTemplate implements CouchbaseOperations { return; } - final ConvertedCouchbaseDocument converted = new ConvertedCouchbaseDocument(); + final CouchbaseDocument converted = new CouchbaseDocument(); couchbaseConverter.write(objectToRemove, converted); execute(new BucketCallback>() { @@ -257,7 +268,7 @@ public class CouchbaseTemplate implements CouchbaseOperations { } private RuntimeException potentiallyConvertRuntimeException(final RuntimeException ex) { - RuntimeException resolved = this.exceptionTranslator.translateExceptionIfPossible(ex); + RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex); return resolved == null ? ex : resolved; } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java index e0956950..f3f6ccdb 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java @@ -29,6 +29,7 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, protected final GenericConversionService conversionService; protected EntityInstantiators instantiators = new EntityInstantiators(); + protected CustomConversions conversions = new CustomConversions(); public AbstractCouchbaseConverter( GenericConversionService conversionService) { 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 index 3975709e..cba4dd0c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java @@ -18,7 +18,7 @@ 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.ConvertedCouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; @@ -27,7 +27,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProper */ public interface CouchbaseConverter extends EntityConverter, - CouchbasePersistentProperty, Object, ConvertedCouchbaseDocument>, - CouchbaseWriter, - EntityReader { + 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..ddb2617c --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseDocumentPropertyAccessor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.convert; + +import org.springframework.context.expression.MapAccessor; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypedValue; + +import java.util.Map; + +/** + * @author Michael Nitschinger + */ +public class CouchbaseDocumentPropertyAccessor extends MapAccessor { + + static MapAccessor INSTANCE = new CouchbaseDocumentPropertyAccessor(); + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {CouchbaseDocument.class}; + } + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) { + return true; + } + + @Override + public TypedValue read(EvaluationContext contect, Object target, 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..3026eb18 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseTypeMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.convert; + +import org.springframework.data.convert.TypeMapper; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; + +/** + * @author Michael Nitschinger + */ +public interface CouchbaseTypeMapper extends TypeMapper { + +} 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..47f53364 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CustomConversions.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.convert; + +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * 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 + */ +public class CustomConversions { + + private final SimpleTypeHolder simpleTypeHolder; + + CustomConversions() { + this(new ArrayList()); + } + + public CustomConversions(final List converters) { + Assert.notNull(converters); + + simpleTypeHolder = new SimpleTypeHolder(); + } + + public boolean isSimpleType(Class type) { + return simpleTypeHolder.isSimpleType(type); + } + +} 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..046329a2 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/DefaultCouchbaseTypeMapper.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.convert; + +import org.springframework.data.convert.DefaultTypeMapper; +import org.springframework.data.convert.TypeAliasAccessor; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; + +/** + * @author Michael Nitschinger + */ +public class DefaultCouchbaseTypeMapper extends DefaultTypeMapper implements CouchbaseTypeMapper { + + public static final String DEFAULT_TYPE_KEY = "_class"; + + 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 index d1185d3d..5206a2fd 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -17,31 +17,26 @@ package org.springframework.data.couchbase.core.convert; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.*; + import org.springframework.beans.BeansException; 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.ConvertedCouchbaseDocument; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; -import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.couchbase.core.mapping.*; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.AssociationHandler; +import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.BeanWrapper; -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.*; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.data.mapping.PropertyHandler; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * @author Michael Nitschinger @@ -53,6 +48,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter protected final MappingContext, CouchbasePersistentProperty> mappingContext; protected boolean useFieldAccessOnly = true; + protected CouchbaseTypeMapper typeMapper; + private SpELContext spELContext; @SuppressWarnings("deprecation") public MappingCouchbaseConverter(MappingContext, @@ -60,6 +57,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter super(ConversionServiceFactory.createDefaultConversionService()); this.mappingContext = mappingContext; + typeMapper = new DefaultCouchbaseTypeMapper(DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY); + + spELContext = new SpELContext(CouchbaseDocumentPropertyAccessor.INSTANCE); } @Override @@ -68,131 +68,360 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter return mappingContext; } - - private ParameterValueProvider getParameterProvider( - CouchbasePersistentEntity entity, ConvertedCouchbaseDocument source, Object parent) { - - CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, parent); - PersistentEntityParameterValueProvider parameterProvider = - new PersistentEntityParameterValueProvider( - entity, provider, parent); - - return parameterProvider; - } - @Override - public R read(Class type, ConvertedCouchbaseDocument doc) { - return read(type, doc, null); - } - - public R read(Class type, final ConvertedCouchbaseDocument doc, Object parent) { - final CouchbasePersistentEntity entity = (CouchbasePersistentEntity) - mappingContext.getPersistentEntity(type); - - ParameterValueProvider provider = - getParameterProvider(entity, doc, parent); - EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); - R instance = instantiator.createInstance(entity, provider); - - final BeanWrapper, R> wrapper = - BeanWrapper.create(instance, conversionService); - final R result = wrapper.getBean(); - - // Set properties not already set in the constructor - entity.doWithProperties(new PropertyHandler() { - public void doWithPersistentProperty(CouchbasePersistentProperty prop) { - - boolean isConstructorProperty = entity.isConstructorArgument(prop); - boolean hasValueForProperty = doc.containsField(prop.getFieldName()); - - if (!hasValueForProperty || isConstructorProperty) { - return; - } - - Object obj = null; - if(prop.isIdProperty()) { - obj = doc.getId(); - } else { - obj = doc.get(prop.getFieldName()); - } - wrapper.setProperty(prop, obj, useFieldAccessOnly); - } - }); - - return result; + public R read(Class clazz, CouchbaseDocument doc) { + return read(ClassTypeInformation.from(clazz), doc, null); } + protected R read(TypeInformation type, CouchbaseDocument doc) { + return read(type, doc, null); + } + + protected R read(TypeInformation type, final CouchbaseDocument source, Object parent) { + + if (source == null) { + return null; + } + + TypeInformation typeToUse = typeMapper.readType(source, type); + Class rawType = typeToUse.getType(); + + if (typeToUse.isMap()) { + return (R) readMap(typeToUse, source, parent); + } + + CouchbasePersistentEntity persistentEntity = (CouchbasePersistentEntity) + mappingContext.getPersistentEntity(typeToUse); + + if (persistentEntity == null) { + throw new MappingException("No mapping metadata found for " + rawType.getName()); + } + + return read(persistentEntity, source, parent); + } + + 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, R> wrapper = BeanWrapper.create(instance, conversionService); + final R result = wrapper.getBean(); + + entity.doWithProperties(new PropertyHandler() { + public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { + if (!source.containsKey(prop.getFieldName()) || entity.isConstructorArgument(prop)) { + return; + } + + Object obj = prop.isIdProperty() ? source.getId() : getValueInternal(prop, source, evaluator, result); + wrapper.setProperty(prop, obj, useFieldAccessOnly); + } + }); + + entity.doWithAssociations(new AssociationHandler() { + public void doWithAssociation(final Association association) { + CouchbasePersistentProperty inverseProp = association.getInverse(); + Object obj = getValueInternal(inverseProp, source, evaluator, result); + + wrapper.setProperty(inverseProp, obj); + } + }); + + return result; + } + + protected Object getValueInternal(CouchbasePersistentProperty prop, CouchbaseDocument source, SpELExpressionEvaluator eval, + Object parent) { + + CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, spELContext, parent); + return provider.getPropertyValue(prop); + } + + private ParameterValueProvider getParameterProvider(CouchbasePersistentEntity entity, + CouchbaseDocument source, DefaultSpELExpressionEvaluator evaluator, Object parent) { + + CouchbasePropertyValueProvider provider = new CouchbasePropertyValueProvider(source, evaluator, parent); + PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider( + entity, provider, parent); + return new ConverterAwareSpELExpressionParameterValueProvider(evaluator, conversionService, parameterProvider, + parent); + } + + protected Map readMap(TypeInformation type, CouchbaseDocument doc, Object parent) { + Assert.notNull(doc); + + Class mapType = typeMapper.readType(doc, type).getType(); + Map map = CollectionFactory.createMap(mapType, doc.export().keySet().size()); + Map sourceMap = doc.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; + } + + private Object getPotentiallyConvertedSimpleRead(Object value, Class target) { + + if (value == null || target == null) { + return value; + } + + if (Enum.class.isAssignableFrom(target)) { + return Enum.valueOf((Class) target, value.toString()); + } + + return target.isAssignableFrom(value.getClass()) ? value : conversionService.convert(value, target); + } + + @Override - public void write(Object source, ConvertedCouchbaseDocument target) { - if(source == null) { + public void write(final Object source, final CouchbaseDocument target) { + if (source == null) { return; } TypeInformation type = ClassTypeInformation.from(source.getClass()); - try { - writeInternal(source, target, type); - } catch (IOException ex) { - throw new MappingException("Could not translate to JSON while converting " - + source.getClass().getName()); - } + typeMapper.writeType(type, target); + writeInternal(source, target, type); + if (target.getId() == null) { + throw new MappingException("An ID property is needed, but not found on this entity."); + } } - protected void writeInternal(final Object source, - ConvertedCouchbaseDocument target, TypeInformation type) - throws IOException { - CouchbasePersistentEntity entity = mappingContext.getPersistentEntity( - source.getClass()); - - if(entity == null) { - throw new MappingException("No mapping metadata found for entity of type " - + source.getClass().getName()); + protected void writeInternal(final Object source, final CouchbaseDocument target, final TypeInformation typeHint) { + if (source == null) { + 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); + } + + protected void writeInternal(final Object source, final CouchbaseDocument target, final CouchbasePersistentEntity entity) { + if (source == null) { + return; + } + + if (entity == null) { + throw new MappingException("No mapping metadata found for entity of type " + source.getClass().getName()); + } + + final BeanWrapper, Object> wrapper = BeanWrapper.create(source, conversionService); final CouchbasePersistentProperty idProperty = entity.getIdProperty(); - if(idProperty == null) { - throw new MappingException("ID property required for entity of type " - + source.getClass().getName()); + if (idProperty != null && target.getId() == null) { + String id = wrapper.getProperty(idProperty, String.class, useFieldAccessOnly); + target.setId(id); } + target.setExpiration(entity.getExpiry()); - final BeanWrapper, Object> wrapper = - BeanWrapper.create(source, conversionService); - - String id = wrapper.getProperty(idProperty, String.class, false); - target.setId(id); - target.setExpiry(entity.getExpiry()); - - JsonFactory jsonFactory = new JsonFactory(); - OutputStream jsonStream = new ByteArrayOutputStream(); - final JsonGenerator jsonGenerator = jsonFactory.createJsonGenerator( - jsonStream, JsonEncoding.UTF8); - jsonGenerator.setCodec(new ObjectMapper()); - - jsonGenerator.writeStartObject(); entity.doWithProperties(new PropertyHandler() { - @Override - public void doWithPersistentProperty(CouchbasePersistentProperty prop) { - if(prop.equals(idProperty)) { + public void doWithPersistentProperty(final CouchbasePersistentProperty prop) { + if (prop.equals(idProperty)) { return; } - Object propertyValue = wrapper.getProperty(prop, prop.getType(), false); - if(propertyValue != null) { - try { - jsonGenerator.writeFieldName(prop.getFieldName()); - jsonGenerator.writeObject(propertyValue); - } catch (IOException ex) { - throw new MappingException("Could not translate to JSON while converting " - + source.getClass().getName()); + Object propertyObj = wrapper.getProperty(prop, prop.getType(), useFieldAccessOnly); + if (null != propertyObj) { + if (!conversions.isSimpleType(propertyObj.getClass())) { + writePropertyInternal(propertyObj, target, prop); + } else { + writeSimpleInternal(propertyObj, target, prop.getFieldName()); } } - } }); - jsonGenerator.writeEndObject(); - jsonGenerator.close(); - target.setRawValue(jsonStream.toString()); + 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, useFieldAccessOnly); + if (null != propertyObj) { + writePropertyInternal(propertyObj, target, inverseProp); + } + } + }); + + } + + 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; + } + + 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); + } + + private CouchbaseDocument createMap(Map map, CouchbasePersistentProperty prop) { + Assert.notNull(map, "Given map must not be null!"); + Assert.notNull(prop, "PersistentProperty must not be null!"); + + return writeMapInternal(map, new CouchbaseDocument(), prop.getTypeInformation()); + } + + private CouchbaseDocument writeMapInternal(Map source, CouchbaseDocument target, TypeInformation type) { + 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(), 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; + } + + private CouchbaseList createCollection(Collection collection, CouchbasePersistentProperty prop) { + return writeCollectionInternal(collection, new CouchbaseList(), prop.getTypeInformation()); + } + + private CouchbaseList writeCollectionInternal(Collection source, CouchbaseList target, 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(), componentType)); + } else { + CouchbaseDocument embeddedDoc = new CouchbaseDocument(); + writeInternal(element, embeddedDoc, componentType); + target.put(embeddedDoc); + } + + } + + return target; + } + + 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()); + } + + + private static Collection asCollection(final Object source) { + + if (source instanceof Collection) { + return (Collection) source; + } + + return source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); + } + + private boolean isSubtype(final Class left, final Class right) { + return left.isAssignableFrom(right) && !left.equals(right); + } + + private void writeSimpleInternal(Object source, CouchbaseDocument target, String key) { + target.put(key, source); + } + + 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 @@ -200,33 +429,71 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter throws BeansException { this.applicationContext = applicationContext; } - - private class CouchbasePropertyValueProvider implements PropertyValueProvider { - private final ConvertedCouchbaseDocument source; - private final Object parent; + private class CouchbasePropertyValueProvider implements PropertyValueProvider { - public CouchbasePropertyValueProvider(ConvertedCouchbaseDocument source, Object parent) { - Assert.notNull(source); - this.source = source; - this.parent = parent; - } + private final CouchbaseDocument source; + private final SpELExpressionEvaluator evaluator; + private final Object parent; - public T getPropertyValue(CouchbasePersistentProperty property) { - T value = null; - - if(property.isIdProperty()) { - value = (T) source.getId(); - } else { - value = (T) source.get(property.getFieldName()); - } - - if (value == null) { - return null; - } + public CouchbasePropertyValueProvider(CouchbaseDocument source, SpELContext factory, Object parent) { + this(source, new DefaultSpELExpressionEvaluator(source, factory), parent); + } - return value; - } - } + public CouchbasePropertyValueProvider(CouchbaseDocument source, DefaultSpELExpressionEvaluator evaluator, Object parent) { + + Assert.notNull(source); + Assert.notNull(evaluator); + + this.source = source; + this.evaluator = evaluator; + this.parent = parent; + } + + public R getPropertyValue(final CouchbasePersistentProperty property) { + + String expression = property.getSpelExpression(); + Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName()); + + if (property.isIdProperty()) { + return (R) source.getId(); + } + + if (value == null) { + return null; + } + + return readValue(value, property.getTypeInformation(), parent); + } + } + + private R readValue(Object value, TypeInformation type, Object parent) { + Class rawType = type.getType(); + + 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); + } + } + + private class ConverterAwareSpELExpressionParameterValueProvider extends + SpELExpressionParameterValueProvider { + private final Object parent; + + public ConverterAwareSpELExpressionParameterValueProvider(SpELExpressionEvaluator evaluator, + ConversionService conversionService, ParameterValueProvider delegate, Object parent) { + + super(evaluator, conversionService, delegate); + this.parent = parent; + } + + @Override + protected T potentiallyConvertSpelValue(Object object, 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..71a34af8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java @@ -0,0 +1,167 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.convert.translation; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.ObjectMapper; +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; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * A Jackson JSON Translator. + * + * @author Michael Nitschinger + */ +public class JacksonTranslationService implements TranslationService { + + private SimpleTypeHolder simpleTypeHolder = new SimpleTypeHolder(); + private JsonFactory factory = new JsonFactory(); + + @Override + public final Object encode(final CouchbaseStorable source) { + OutputStream stream = new ByteArrayOutputStream(); + + try { + JsonGenerator generator = factory.createGenerator(stream, JsonEncoding.UTF8); + encodeRecursive(source, generator); + generator.close(); + } catch (IOException ex) { + throw new RuntimeException("Could not encode JSON", ex); + } + + return stream.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; + } + + if (simpleTypeHolder.isSimpleType(value.getClass())) { + generator.writeObject(value); + } else { + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(generator, value); + } + + } + + generator.writeEndObject(); + } + + @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; + } + + 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; + } + + 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; + } + + 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: + return parser.getValueAsInt(); + case VALUE_NUMBER_FLOAT: + return parser.getValueAsDouble(); + default: + throw new MappingException("Could not decode primitve value " + token); + } + } + + +} 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..d0535326 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/translation/TranslationService.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.convert.translation; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.CouchbaseStorable; + +/** + * @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. + */ + T 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(T source, CouchbaseStorable target); +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/ConvertedCouchbaseDocument.java b/src/main/java/org/springframework/data/couchbase/core/mapping/ConvertedCouchbaseDocument.java deleted file mode 100644 index ddd8cc61..00000000 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/ConvertedCouchbaseDocument.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.couchbase.core.mapping; - -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.data.mapping.model.MappingException; - -/** - * @author Michael Nitschinger - */ -public class ConvertedCouchbaseDocument { - - private String id; - - private String rawValue; - - private int expiry; - - private Map decoded; - - public ConvertedCouchbaseDocument() { - this("", "", 0); - } - - public ConvertedCouchbaseDocument(String id, String rawValue) { - this(id, rawValue, 0); - } - - public ConvertedCouchbaseDocument(String id, String rawValue, int expiry) { - this.id = id; - this.rawValue = rawValue; - this.expiry = expiry; - this.decoded = new HashMap(); - parseJson(); - } - - public void setId(String id) { - this.id = id; - } - - public String getId() { - return id; - } - - public String getRawValue() { - return rawValue; - } - - public void setRawValue(String value) { - this.rawValue = value; - parseJson(); - - } - - public int getExpiry() { - return expiry; - } - - public void setExpiry(int expiry) { - this.expiry = expiry; - } - - public boolean containsField(String fieldname) { - return decoded.containsKey(fieldname); - } - - public Object get(String fieldname) { - return decoded.get(fieldname); - } - - private void parseJson() { - ObjectMapper mapper = new ObjectMapper(); - try { - if(!getRawValue().isEmpty()) { - Map converted = mapper.readValue(getRawValue(), - new TypeReference>() { }); - this.decoded = converted; - } - } catch(Exception e) { - throw new MappingException("Error while decoding JSON object.", e); - } - } - -} 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..3cd362cf --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseDocument.java @@ -0,0 +1,278 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.mapping; + +import org.springframework.data.mapping.model.SimpleTypeHolder; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 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.getClass()); + + 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; + } + + 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 clazz the class type to check and verify. + */ + private void verifyValueType(final Class clazz) { + if (simpleTypeHolder.isSimpleType(clazz)) { + return; + } + + throw new IllegalArgumentException("Attribute of type " + + clazz.getCanonicalName() + " can not be stored and must be converted."); + } + + @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..2d1fc479 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseList.java @@ -0,0 +1,148 @@ +package org.springframework.data.couchbase.core.mapping; + +import org.springframework.data.mapping.model.SimpleTypeHolder; + +import java.util.*; + +/** + * 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; + + public CouchbaseList() { + this(new ArrayList()); + } + + public CouchbaseList(List initialPayload) { + payload = initialPayload; + + Set> additionalTypes = new HashSet>(); + additionalTypes.add(CouchbaseDocument.class); + additionalTypes.add(CouchbaseList.class); + simpleTypeHolder = new SimpleTypeHolder(additionalTypes, true); + } + + public final CouchbaseList put(Object value) { + verifyValueType(value.getClass()); + + payload.add(value); + return this; + } + + public final Object get(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); + } + + 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 clazz the class type to check and verify. + */ + private void verifyValueType(final Class clazz) { + if (simpleTypeHolder.isSimpleType(clazz)) { + return; + } + + throw new IllegalArgumentException("Attribute of type " + + clazz.getCanonicalName() + "can not be stored and must be converted."); + } + + @Override + public String toString() { + return "CouchbaseList{" + + "payload=" + payload + + '}'; + } +} 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..bdcf4f15 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseStorable.java @@ -0,0 +1,5 @@ +package org.springframework.data.couchbase.core.mapping; + +public interface CouchbaseStorable { + +} diff --git a/src/main/resources/META-INF/spring.handlers b/src/main/resources/META-INF/spring.handlers new file mode 100644 index 00000000..abeeba42 --- /dev/null +++ b/src/main/resources/META-INF/spring.handlers @@ -0,0 +1 @@ +http\://www.springframework.org/schema/data/couchbase=org.springframework.data.couchbase.config.CouchbaseNamespaceHandler \ No newline at end of file diff --git a/src/main/resources/META-INF/spring.schemas b/src/main/resources/META-INF/spring.schemas new file mode 100644 index 00000000..349e29ce --- /dev/null +++ b/src/main/resources/META-INF/spring.schemas @@ -0,0 +1,2 @@ +http\://www.springframework.org/schema/data/couchbase/spring-couchbase-1.0.xsd=org/springframework/data/couchbase/config/spring-couchbase-1.0.xsd +http\://www.springframework.org/schema/data/couchbase/spring-couchbase.xsd=org/springframework/data/couchbase/config/spring-couchbase-1.0.xsd \ No newline at end of file diff --git a/src/main/resources/META-INF/spring.tooling b/src/main/resources/META-INF/spring.tooling new file mode 100644 index 00000000..382c2816 --- /dev/null +++ b/src/main/resources/META-INF/spring.tooling @@ -0,0 +1,4 @@ +# Tooling related information for the Couchbase DB namespace +http\://www.springframework.org/schema/data/couchbase@name=Couchbase Namespace +http\://www.springframework.org/schema/data/couchbase@prefix=couchbase +http\://www.springframework.org/schema/data/couchbase@icon=org/springframework/jdbc/config/spring-jdbc.gif \ No newline at end of file diff --git a/src/main/resources/org/springframework/data/couchbase/config/spring-couchbase-1.0.xsd b/src/main/resources/org/springframework/data/couchbase/config/spring-couchbase-1.0.xsd new file mode 100644 index 00000000..399d96e0 --- /dev/null +++ b/src/main/resources/org/springframework/data/couchbase/config/spring-couchbase-1.0.xsd @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateTest.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateTest.java index 6571e587..8a230d63 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateTest.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateTest.java @@ -56,11 +56,12 @@ public class CouchbaseTemplateTest { template.save(beer); String result = (String) client.get(id); - String expected = "{\"is_active\":" + active + ",\"name\":\"" + name + "\"}"; + String expected = "{\"_class\":\"org.springframework.data.couchbase.core.Beer\"" + + ",\"is_active\":false,\"name\":\"The Awesome Stout\"}"; assertNotNull(result); assertEquals(expected, result); } - + @Test public void saveDocumentWithExpiry() throws Exception { String id = "simple-doc-with-expiry"; @@ -70,23 +71,25 @@ public class CouchbaseTemplateTest { Thread.sleep(3000); assertNull(client.get(id)); } - + @Test public void insertDoesNotOverride() { - String id ="double-insert-test"; - String expected = "{\"name\":\"Mr. A\"}"; + String id ="double-insert-test"; + String expected = "{\"_class\":\"org.springframework.data.couchbase.core." + + "CouchbaseTemplateTest$SimplePerson\",\"name\":\"Mr. A\"}"; - SimplePerson doc = new SimplePerson(id, "Mr. A"); - template.insert(doc); - String result = (String) client.get(id); - assertEquals(expected, result); - - doc = new SimplePerson(id, "Mr. B"); - template.insert(doc); - result = (String) client.get(id); - assertEquals(expected, result); + SimplePerson doc = new SimplePerson(id, "Mr. A"); + template.insert(doc); + String result = (String) client.get(id); + assertEquals(expected, result); + + doc = new SimplePerson(id, "Mr. B"); + template.insert(doc); + result = (String) client.get(id); + assertEquals(expected, result); } - + + @Test public void updateDoesNotInsert() { String id ="update-does-not-insert"; @@ -94,25 +97,11 @@ public class CouchbaseTemplateTest { template.update(doc); assertNull(client.get(id)); } - - @Test - public void validFindById() { - String id = "beers:findme-stout"; - String name = "The Findme Stout"; - boolean active = true; - Beer beer = new Beer(id).setName(name).setActive(active); - template.save(beer); - - Beer found = template.findById(id, Beer.class); - assertNotNull(found); - assertEquals(id, found.getId()); - assertEquals(name, found.getName()); - assertEquals(active, found.getActive()); - } + @Test public void removeDocument() { - String id = "beers:findme-stout"; + String id = "beers:awesome-stout"; Object result = client.get(id); assertNotNull(result); @@ -123,6 +112,7 @@ public class CouchbaseTemplateTest { assertNull(result); } + @Test public void storeListsAndMaps() { String id ="persons:lots-of-names"; @@ -139,8 +129,10 @@ public class CouchbaseTemplateTest { template.save(complex); - String expected = "{\"firstnames\":[\"Michael\",\"Thomas\"],\"info2\":{}," + - "\"info1\":{\"foo\":true,\"bar\":false},\"votes\":[]}"; + String expected = "{\"_class\":\"org.springframework.data.couchbase.core." + + "CouchbaseTemplateTest$ComplexPerson\",\"info1\":{\"foo\":true,\"bar\"" + + ":false},\"votes\":[],\"firstnames\":[\"Michael\",\"Thomas\"],\"info2\":" + + "{}}"; assertEquals(expected, client.get(id)); ComplexPerson response = template.findById(id, ComplexPerson.class); @@ -150,6 +142,23 @@ public class CouchbaseTemplateTest { assertEquals(info1, response.getInfo1()); assertEquals(info2, response.getInfo2()); } + + + @Test + public void validFindById() { + String id = "beers:findme-stout"; + String name = "The Findme Stout"; + boolean active = true; + Beer beer = new Beer(id).setName(name).setActive(active); + template.save(beer); + + Beer found = template.findById(id, Beer.class); + + assertNotNull(found); + assertEquals(id, found.getId()); + assertEquals(name, found.getName()); + assertEquals(active, found.getActive()); + } /** * A sample document with just an id and property. diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTest.java b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTest.java new file mode 100644 index 00000000..13e81e45 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTest.java @@ -0,0 +1,487 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core.mapping; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.TestApplicationConfig; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.*; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Michael Nitschinger + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestApplicationConfig.class) +public class MappingCouchbaseConverterTest { + + @Autowired + private MappingCouchbaseConverter converter; + + @Test + public void shouldNotThrowNPE() { + CouchbaseDocument converted = new CouchbaseDocument(); + converter.write(null, converted); + assertNull(converted.getId()); + assertEquals(0, converted.getExpiration()); + } + + @Test(expected = MappingException.class) + public void doesNotAllowSimpleType1() { + converter.write("hello", new CouchbaseDocument()); + } + + @Test(expected = MappingException.class) + public void doesNotAllowSimpleType2() { + converter.write(true, new CouchbaseDocument()); + } + + @Test(expected = MappingException.class) + public void doesNotAllowSimpleType3() { + converter.write(42, new CouchbaseDocument()); + } + + @Test(expected = MappingException.class) + public void needsIDOnEntity() { + converter.write(new EntityWithoutID("foo"), + new CouchbaseDocument()); + } + + + @Test + public void writesString() { + CouchbaseDocument converted = new CouchbaseDocument(); + StringEntity entity = new StringEntity("foobar"); + + Map expected = new HashMap(); + expected.put("_class", entity.getClass().getName()); + expected.put("attr0", "foobar"); + + converter.write(entity, converted); + assertEquals(expected.toString(), converted.export().toString()); + assertEquals(BaseEntity.ID, converted.getId()); + } + + @Test + public void readsString() { + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", StringEntity.class.getName()); + source.put("attr0", "foobar"); + + StringEntity converted = converter.read(StringEntity.class, source); + assertEquals("foobar", converted.attr0); + } + + + @Test + public void writesNumber() { + CouchbaseDocument converted = new CouchbaseDocument(); + NumberEntity entity = new NumberEntity(42); + + Map expected = new HashMap(); + expected.put("_class", entity.getClass().getName()); + expected.put("attr0", 42); + + converter.write(entity, converted); + assertEquals(expected.toString(), converted.export().toString()); + assertEquals(BaseEntity.ID, converted.getId()); + } + + @Test + public void readsNumber() { + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", NumberEntity.class.getName()); + source.put("attr0", 42); + + NumberEntity converted = converter.read(NumberEntity.class, source); + assertEquals(42, converted.attr0); + } + + + @Test + public void writesBoolean() { + CouchbaseDocument converted = new CouchbaseDocument(); + BooleanEntity entity = new BooleanEntity(true); + + Map expected = new HashMap(); + expected.put("_class", entity.getClass().getName()); + expected.put("attr0", true); + + converter.write(entity, converted); + assertEquals(expected.toString(), converted.export().toString()); + assertEquals("mockid", converted.getId()); + } + + @Test + public void readsBoolean() { + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", BooleanEntity.class.getName()); + source.put("attr0", true); + + BooleanEntity converted = converter.read(BooleanEntity.class, source); + assertTrue(converted.attr0); + } + + @Test + public void writesMixedSimpleTypes() { + CouchbaseDocument converted = new CouchbaseDocument(); + MixedSimpleEntity entity = new MixedSimpleEntity("a", 5, -0.3, true); + + Map expected = new HashMap(); + expected.put("_class", entity.getClass().getName()); + expected.put("attr0", "a"); + expected.put("attr1", 5); + expected.put("attr2", -0.3); + expected.put("attr3", true); + + converter.write(entity, converted); + assertEquals(expected.toString(), converted.export().toString()); + } + + @Test + public void readsMixedSimpleTypes() { + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", MixedSimpleEntity.class.getName()); + source.put("attr0", "a"); + source.put("attr1", 5); + source.put("attr2", -0.3); + source.put("attr3", true); + + MixedSimpleEntity converted = converter.read(MixedSimpleEntity.class, source); + assertEquals("a", converted.attr0); + assertEquals(5, converted.attr1); + assertEquals(-0.3, converted.attr2, 0); + assertTrue(converted.attr3); + } + + + @Test + public void writesUninitializedValues() { + CouchbaseDocument converted = new CouchbaseDocument(); + UninitializedEntity entity = new UninitializedEntity(); + + Map expected = new HashMap(); + expected.put("_class", entity.getClass().getName()); + expected.put("attr1", 0); + + converter.write(entity, converted); + assertEquals(expected.toString(), converted.export().toString()); + } + + @Test + public void readsUninitializedValues() { + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", UninitializedEntity.class.getName()); + source.put("attr1", 0); + + UninitializedEntity converted = converter.read(UninitializedEntity.class, source); + assertNull(converted.attr0); + assertEquals(0, converted.attr1); + assertNull(converted.attr2); + } + + @Test + public void writesAndReadsMapsAndNestedMaps() { + CouchbaseDocument converted = new CouchbaseDocument(); + + Map attr0 = new HashMap(); + Map attr1 = new TreeMap(); + Map attr2 = new LinkedHashMap(); + Map> attr3 = + new HashMap>(); + + attr0.put("foo", "bar"); + attr1.put("bar", true); + attr3.put("hashmap", attr0); + + MapEntity entity = new MapEntity(attr0, attr1, attr2, attr3); + + Map expected = new HashMap(); + expected.put("_class", entity.getClass().getName()); + expected.put("attr0", attr0); + expected.put("attr1", attr1); + expected.put("attr2", attr2); + expected.put("attr3", attr3); + + converter.write(entity, converted); + assertEquals(expected.toString(), converted.export().toString()); + + + CouchbaseDocument cattr0 = new CouchbaseDocument(); + cattr0.put("foo", "bar"); + + CouchbaseDocument cattr1 = new CouchbaseDocument(); + cattr1.put("bar", true); + + CouchbaseDocument cattr2 = new CouchbaseDocument(); + + CouchbaseDocument cattr3 = new CouchbaseDocument(); + cattr3.put("hashmap", cattr0); + + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", MapEntity.class.getName()); + source.put("attr0", cattr0); + source.put("attr1", cattr1); + source.put("attr2", cattr2); + source.put("attr3", cattr3); + + MapEntity readConverted = converter.read(MapEntity.class, source); + assertEquals(attr0, readConverted.attr0); + assertEquals(attr1, readConverted.attr1); + assertEquals(attr2, readConverted.attr2); + assertEquals(attr3, readConverted.attr3); + } + + @Test + public void writesAndReadsListAndNestedList() { + CouchbaseDocument converted = new CouchbaseDocument(); + List attr0 = new ArrayList(); + List attr1 = new LinkedList(); + List> attr2 = new ArrayList>(); + + attr0.add("foo"); + attr0.add("bar"); + attr2.add(attr0); + + ListEntity entity = new ListEntity(attr0, attr1, attr2); + + Map expected = new HashMap(); + expected.put("_class", entity.getClass().getName()); + expected.put("attr0", attr0); + expected.put("attr1", attr1); + expected.put("attr2", attr2); + + converter.write(entity, converted); + assertEquals(expected.toString(), converted.export().toString()); + + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", ListEntity.class.getName()); + CouchbaseList cattr0 = new CouchbaseList(); + cattr0.put("foo"); + cattr0.put("bar"); + CouchbaseList cattr1 = new CouchbaseList(); + CouchbaseList cattr2 = new CouchbaseList(); + cattr2.put(cattr0); + source.put("attr0", cattr0); + source.put("attr1", cattr1); + source.put("attr2", cattr2); + + ListEntity readConverted = converter.read(ListEntity.class, source); + System.out.println(readConverted.attr0); + System.out.println(readConverted.attr1); + System.out.println(readConverted.attr2); + } + + @Test + public void writesAndReadsSetAndNestedSet() { + CouchbaseDocument converted = new CouchbaseDocument(); + Set attr0 = new HashSet(); + TreeSet attr1 = new TreeSet(); + Set> attr2 = new HashSet>(); + + attr0.add("foo"); + attr0.add("bar"); + attr2.add(attr0); + + SetEntity entity = new SetEntity(attr0, attr1, attr2); + Map expected = new HashMap(); + expected.put("_class", entity.getClass().getName()); + expected.put("attr0", attr0); + expected.put("attr1", attr1); + expected.put("attr2", attr2); + + converter.write(entity, converted); + assertEquals(expected.toString(), converted.export().toString()); + + CouchbaseList cattr0 = new CouchbaseList(); + cattr0.put("foo"); + cattr0.put("bar"); + + CouchbaseList cattr1 = new CouchbaseList(); + + CouchbaseList cattr2 = new CouchbaseList(); + cattr2.put(cattr0); + + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", SetEntity.class.getName()); + source.put("attr0", cattr0); + source.put("attr1", cattr1); + source.put("attr2", cattr2); + + SetEntity readConverted = converter.read(SetEntity.class, source); + assertEquals(attr0, readConverted.attr0); + assertEquals(attr1, readConverted.attr1); + assertEquals(attr2, readConverted.attr2); + } + + + @Test + public void writesAndReadsValueClass() { + CouchbaseDocument converted = new CouchbaseDocument(); + + final String email = "foo@bar.com"; + final Email addy = new Email(email); + List listOfEmails = new ArrayList(); + listOfEmails.add(addy); + + ValueEntity entity = new ValueEntity(addy, listOfEmails); + converter.write(entity, converted); + + Map expected = new HashMap(); + expected.put("_class", entity.getClass().getName()); + expected.put("email", new HashMap() {{ + put("emailAddr", email); + }}); + expected.put("listOfEmails", new ArrayList() {{ + add(new HashMap() {{ + put("emailAddr", email); + }}); + }}); + + assertEquals(expected.toString(), converted.export().toString()); + + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", ValueEntity.class.getName()); + CouchbaseDocument emailDoc = new CouchbaseDocument(); + emailDoc.put("emailAddr", "foo@bar.com"); + source.put("email", emailDoc); + CouchbaseList listOfEmailsDoc = new CouchbaseList(); + listOfEmailsDoc.put(emailDoc); + source.put("listOfEmails", listOfEmailsDoc); + + ValueEntity readConverted = converter.read(ValueEntity.class, source); + assertEquals(addy.emailAddr, readConverted.email.emailAddr); + assertEquals(listOfEmails.get(0).emailAddr, + readConverted.listOfEmails.get(0).emailAddr); + } + + static class EntityWithoutID { + private String attr0; + public EntityWithoutID(String a0) { + attr0 = a0; + } + } + + static class BaseEntity { + public static final String ID = "mockid"; + @Id + private String id = ID; + } + + static class StringEntity extends BaseEntity { + private String attr0; + public StringEntity(String attr0) { + this.attr0 = attr0; + } + } + + static class NumberEntity extends BaseEntity { + private long attr0; + public NumberEntity(long attr0) { + this.attr0 = attr0; + } + } + + static class BooleanEntity extends BaseEntity { + private boolean attr0; + public BooleanEntity(boolean attr0) { + this.attr0 = attr0; + } + } + + static class MixedSimpleEntity extends BaseEntity { + private String attr0; + private int attr1; + private double attr2; + private boolean attr3; + public MixedSimpleEntity(String attr0, int attr1, double attr2, boolean attr3) { + this.attr0 = attr0; + this.attr1 = attr1; + this.attr2 = attr2; + this.attr3 = attr3; + } + } + + static class UninitializedEntity extends BaseEntity { + private String attr0 = null; + private int attr1; + private Integer attr2; + } + + static class MapEntity extends BaseEntity { + private Map attr0; + private Map attr1; + private Map attr2; + private Map> attr3; + public MapEntity(Map attr0, Map attr1, Map attr2, Map attr3) { + this.attr0 = attr0; + this.attr1 = attr1; + this.attr2 = attr2; + this.attr3 = attr3; + } + } + + static class ListEntity extends BaseEntity { + private List attr0; + private List attr1; + private List> attr2; + ListEntity(List attr0, List attr1, List> attr2) { + this.attr0 = attr0; + this.attr1 = attr1; + this.attr2 = attr2; + } + } + + static class SetEntity extends BaseEntity { + private Set attr0; + private Set attr1; + private Set> attr2; + SetEntity(Set attr0, Set attr1, Set> attr2) { + this.attr0 = attr0; + this.attr1 = attr1; + this.attr2 = attr2; + } + } + + static class ValueEntity extends BaseEntity { + private Email email; + private List listOfEmails; + + public ValueEntity(Email email, List listOfEmails) { + this.email = email; + this.listOfEmails = listOfEmails; + } + } + + static class Email { + private String emailAddr; + public Email(String emailAddr) { + this.emailAddr = emailAddr; + } + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTest.java b/src/test/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTest.java index 9c6bd279..211e2315 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/SimpleCouchbaseRepositoryTest.java @@ -26,7 +26,7 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import static junit.framework.Assert.assertNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue;