diff --git a/.gitignore b/.gitignore index e08c7941..bc8404d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ target/ .DS_Store + +.classpath +.project +.settings/* diff --git a/README.md b/README.md index 952d036e..fb8d21fd 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ It is distributed from the [Couchbase Maven Repository](http://files.couchbase.c ``` Currently, the project depends on the following packages: - * couchbase.couchbase-client: 1.1.0 - * org.springframework.spring-context: 3.1.3.RELEASE + * couchbase.couchbase-client: 1.1.2 + * org.springframework.spring-context: 3.2.1.RELEASE * cglib.cglib: 2.2.2 * (When Testing) junit.junit: 4.11 diff --git a/pom.xml b/pom.xml index fb05ef39..6b82ee93 100644 --- a/pom.xml +++ b/pom.xml @@ -41,12 +41,12 @@ couchbase couchbase-client - 1.1.0 + 1.1.2 org.springframework spring-context - 3.1.3.RELEASE + 3.2.1.RELEASE jar @@ -63,9 +63,24 @@ org.springframework spring-test - 3.1.3.RELEASE + 3.2.1.RELEASE test + + org.springframework.data + spring-data-commons-core + 1.4.0.RELEASE + + + org.codehaus.jackson + jackson-mapper-asl + 1.9.12 + + + org.springframework + spring-tx + 3.2.1.RELEASE + spring-data-couchbase diff --git a/src/main/java/com/couchbase/spring/cache/CouchbaseCache.java b/src/main/java/com/couchbase/spring/cache/CouchbaseCache.java index adc0b9fb..1be3be41 100644 --- a/src/main/java/com/couchbase/spring/cache/CouchbaseCache.java +++ b/src/main/java/com/couchbase/spring/cache/CouchbaseCache.java @@ -27,8 +27,8 @@ import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper; /** - * The CouchbaseCache class implements the common cache operations on top of - * a CouchbaseClient instance. + * The CouchbaseCache class implements the Spring Cache interface + * on top of Couchbase Server and the Couchbase Java SDK. */ public class CouchbaseCache implements Cache { diff --git a/src/main/java/com/couchbase/spring/cache/CouchbaseCacheManager.java b/src/main/java/com/couchbase/spring/cache/CouchbaseCacheManager.java index 42ff2f2e..213c71ea 100644 --- a/src/main/java/com/couchbase/spring/cache/CouchbaseCacheManager.java +++ b/src/main/java/com/couchbase/spring/cache/CouchbaseCacheManager.java @@ -31,8 +31,11 @@ import org.springframework.cache.Cache; import org.springframework.cache.support.AbstractCacheManager; /** - * The CouchbaseCacheManager handles CouchbaseClient connections and - * orchestrates them as needed. + * The CouchbaseCacheManager orchestrates CouchbaseCache instances. + * + * Since more than one current CouchbaseClient connection can be used + * for caching, the CouchbaseCacheManager orchestrates and handles + * them for the Spring Cache abstraction layer. */ public class CouchbaseCacheManager extends AbstractCacheManager { diff --git a/src/main/java/com/couchbase/spring/config/AbstractCouchbaseConfiguration.java b/src/main/java/com/couchbase/spring/config/AbstractCouchbaseConfiguration.java new file mode 100644 index 00000000..fa1b21f7 --- /dev/null +++ b/src/main/java/com/couchbase/spring/config/AbstractCouchbaseConfiguration.java @@ -0,0 +1,121 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.config; + +import com.couchbase.client.CouchbaseClient; +import com.couchbase.spring.core.CouchbaseMappingContext; +import com.couchbase.spring.core.CouchbaseTemplate; +import com.couchbase.spring.core.convert.MappingCouchbaseConverter; +import com.couchbase.spring.core.mapping.Document; +import java.util.HashSet; +import java.util.Set; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.data.annotation.Persistent; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Base class for Spring Data Couchbase configuration using JavaConfig. + */ +@Configuration +public abstract class AbstractCouchbaseConfiguration { + + /** + * Return the {@link CouchbaseClient} instance to connect to. + */ + @Bean + public abstract CouchbaseClient couchbaseClient() throws Exception; + + /** + * Creates a {@link CouchbaseTemplate}. + */ + @Bean + public CouchbaseTemplate couchbaseTemplate() throws Exception { + return new CouchbaseTemplate(couchbaseClient(), mappingCouchbaseConverter()); + } + + /** + * Creates a {@link MappingCouchbaseConverter} using the configured {@link + * #couchbaseMappingContext}. + */ + @Bean + public MappingCouchbaseConverter mappingCouchbaseConverter() throws Exception { + return new MappingCouchbaseConverter(couchbaseMappingContext()); + } + + /** + * Creates a {@link CouchbaseMappingContext} equipped with entity classes + * scanned from the mapping base package. + */ + @Bean + public CouchbaseMappingContext couchbaseMappingContext() throws Exception { + CouchbaseMappingContext mappingContext = new CouchbaseMappingContext(); + mappingContext.setInitialEntitySet(getInitialEntitySet()); + return mappingContext; + } + + /** + * Scans the mapping base package for classes annotated with {@link Document}. + */ + protected Set> getInitialEntitySet() throws ClassNotFoundException { + String basePackage = getMappingBasePackage(); + Set> initialEntitySet = new HashSet>(); + + if(StringUtils.hasText(basePackage)) { + ClassPathScanningCandidateComponentProvider componentProvider = + new ClassPathScanningCandidateComponentProvider(false); + componentProvider.addIncludeFilter( + new AnnotationTypeFilter(Document.class)); + componentProvider.addIncludeFilter( + new AnnotationTypeFilter(Persistent.class)); + + for (BeanDefinition candidate : + componentProvider.findCandidateComponents(basePackage)) { + initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), + AbstractCouchbaseConfiguration.class.getClassLoader())); + } + } + + return initialEntitySet; + } + + /** + * Return the base package to scan for mapped {@link Document}s. Will return + * the package name of the configuration class (the concrete class, not this + * one here) by default. So if you have a {@code com.acme.AppConfig} extending + * {@link AbstractCouchbaseConfiguration} the base package will be considered + * {@code com.acme} unless the method is overridden to implement alternate + * behavior. + * + * @return the base package to scan for mapped {@link Document} classes or + * {@literal null} to not enable scanning for entities. + */ + protected String getMappingBasePackage() { + return getClass().getPackage().getName(); + } + +} diff --git a/src/main/java/com/couchbase/spring/core/CouchbaseExceptionTranslator.java b/src/main/java/com/couchbase/spring/core/CouchbaseExceptionTranslator.java new file mode 100644 index 00000000..271a9a9d --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/CouchbaseExceptionTranslator.java @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.support.PersistenceExceptionTranslator; + +import com.couchbase.client.ObservedException; +import com.couchbase.client.ObservedModifiedException; +import com.couchbase.client.ObservedTimeoutException; +import com.couchbase.client.vbucket.ConnectionException; + +/** + * Simple {@link PersistenceExceptionTranslator} for Couchbase. + * + * Convert the given runtime exception to an appropriate exception from the + * {@code org.springframework.dao} hierarchy. Return {@literal null} if no translation + * is appropriate: any other exception may have resulted from user code, and should not + * be translated. + */ +public class CouchbaseExceptionTranslator implements PersistenceExceptionTranslator { + + @Override + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + if(ex instanceof ConnectionException) { + return new DataAccessResourceFailureException(ex.getMessage(), ex); + } + + if(ex instanceof ObservedException || + ex instanceof ObservedTimeoutException || + ex instanceof ObservedModifiedException) { + return new DataIntegrityViolationException(ex.getMessage(), ex); + } + + return null; + } + +} diff --git a/src/main/java/com/couchbase/spring/core/CouchbaseMappingContext.java b/src/main/java/com/couchbase/spring/core/CouchbaseMappingContext.java new file mode 100644 index 00000000..c1740f7e --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/CouchbaseMappingContext.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core; + +import com.couchbase.spring.core.mapping.BasicCouchbasePersistentEntity; +import com.couchbase.spring.core.mapping.BasicCouchbasePersistentProperty; +import com.couchbase.spring.core.mapping.CouchbasePersistentProperty; +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.SimpleTypeHolder; +import org.springframework.data.util.TypeInformation; + +public class CouchbaseMappingContext + extends AbstractMappingContext, CouchbasePersistentProperty> + implements ApplicationContextAware { + + private ApplicationContext context; + + @Override + protected BasicCouchbasePersistentEntity createPersistentEntity(TypeInformation typeInformation) { + BasicCouchbasePersistentEntity entity = new BasicCouchbasePersistentEntity(typeInformation); + if(context != null) { + entity.setApplicationContext(context); + } + return entity; + } + + @Override + protected CouchbasePersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor, BasicCouchbasePersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + return new BasicCouchbasePersistentProperty(field, descriptor, owner, simpleTypeHolder); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.context = applicationContext; + super.setApplicationContext(applicationContext); + } + +} diff --git a/src/main/java/com/couchbase/spring/core/CouchbaseOperations.java b/src/main/java/com/couchbase/spring/core/CouchbaseOperations.java new file mode 100644 index 00000000..02efe96e --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/CouchbaseOperations.java @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core; + +import java.util.Collection; + +public interface CouchbaseOperations { + + /** + * Save the given object. + * + * When the document already exists (specified by its unique id), + * then it will be overriden. Otherwise it will be created. + * + *

+ * The object is converted to a JSON representation using an instance of + * {@link CouchbaseConverter}. + *

+ * + * @param objectToSave the object to store in the bucket. + */ + void save(Object objectToSave); + + /** + * Save a list of objects. + * + * When one of the documents already exists (specified by its unique id), + * then it will be overriden. Otherwise it will be created. + * + * @param batchToSave the list of objects to store in the bucket. + */ + void save(Collection batchToSave); + + /** + * Insert the given object. + * + * When the document already exists (specified by its unique id), + * then it will not be overriden. Use the {@link CouchbaseOperations#save} + * method for this. + * + *

+ * The object is converted to a JSON representation using an instance of + * {@link CouchbaseConverter}. + *

+ * + * @param objectToSave the object to add to the bucket. + */ + void insert(Object objectToSave); + + /** + * Insert a list of objects. + * + * When one of the documents already exists (specified by its unique id), + * then it will not be overriden. Use the {@link CouchbaseOperations#save} + * method for this. + * + * @param batchToSave the list of objects to add to the bucket. + */ + void insert(Collection batchToSave); + + /** + * Update the given object. + * + * When the document does not exists (specified by its unique id), + * then it will not be created. Use the {@link CouchbaseOperations#save} + * method for this. + * + *

+ * The object is converted to a JSON representation using an instance of + * {@link CouchbaseConverter}. + *

+ * + * @param objectToSave the object to add to the bucket. + */ + void update(Object objectToSave); + + /** + * Insert a list of objects. + * + * When one of the documents does not exists (specified by its unique id), + * then it will not be created. Use the {@link CouchbaseOperations#save} + * method for this. + * + * @param batchToSave the list of objects to add to the bucket. + */ + void update(Collection batchToSave); + + /** + * Find an object by its given Id and map it to the corresponding entity. + * + * @param id the unique ID of the document. + * @param entityClass the entity to map to. + * @return returns the found object or null otherwise. + */ + public T findById(String id, Class entityClass); +} diff --git a/src/main/java/com/couchbase/spring/core/CouchbaseTemplate.java b/src/main/java/com/couchbase/spring/core/CouchbaseTemplate.java new file mode 100644 index 00000000..f0628fd1 --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/CouchbaseTemplate.java @@ -0,0 +1,150 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.springframework.data.mapping.context.MappingContext; + +import com.couchbase.client.CouchbaseClient; +import com.couchbase.spring.core.convert.CouchbaseConverter; +import com.couchbase.spring.core.convert.MappingCouchbaseConverter; +import com.couchbase.spring.core.mapping.ConvertedCouchbaseDocument; +import com.couchbase.spring.core.mapping.CouchbasePersistentEntity; +import com.couchbase.spring.core.mapping.CouchbasePersistentProperty; + +public class CouchbaseTemplate implements CouchbaseOperations { + + private CouchbaseClient client; + private CouchbaseConverter couchbaseConverter; + protected final MappingContext, + CouchbasePersistentProperty> mappingContext; + private static final Collection ITERABLE_CLASSES; + private final CouchbaseExceptionTranslator exceptionTranslator = + new CouchbaseExceptionTranslator(); + + static { + Set iterableClasses = new HashSet(); + iterableClasses.add(List.class.getName()); + iterableClasses.add(Collection.class.getName()); + iterableClasses.add(Iterator.class.getName()); + ITERABLE_CLASSES = Collections.unmodifiableCollection(iterableClasses); + } + + public CouchbaseTemplate(CouchbaseClient client) { + this(client, null); + } + + public CouchbaseTemplate(CouchbaseClient client, CouchbaseConverter converter) { + this.client = client; + this.couchbaseConverter = converter == null ? getDefaultConverter(client) : converter; + this.mappingContext = this.couchbaseConverter.getMappingContext(); + } + + private CouchbaseConverter getDefaultConverter(CouchbaseClient client) { + MappingCouchbaseConverter converter = new MappingCouchbaseConverter( + new CouchbaseMappingContext()); + converter.afterPropertiesSet(); + return converter; + } + + public void insert(Object objectToSave) { + ensureNotIterable(objectToSave); + + ConvertedCouchbaseDocument converted = new ConvertedCouchbaseDocument(); + couchbaseConverter.write(objectToSave, converted); + client.add(converted.getId(), converted.getExpiry(), converted.getRawValue()); + } + + public void insert(Collection batchToSave) { + Iterator iter = batchToSave.iterator(); + while(iter.hasNext()) { + insert(iter.next()); + } + } + + public void save(Object objectToSave) { + ensureNotIterable(objectToSave); + + ConvertedCouchbaseDocument converted = new ConvertedCouchbaseDocument(); + couchbaseConverter.write(objectToSave, converted); + client.set(converted.getId(), converted.getExpiry(), converted.getRawValue()); + } + + public void save(Collection batchToSave) { + Iterator iter = batchToSave.iterator(); + while(iter.hasNext()) { + save(iter.next()); + } + } + + public void update(Object objectToSave) { + ensureNotIterable(objectToSave); + + ConvertedCouchbaseDocument converted = new ConvertedCouchbaseDocument(); + couchbaseConverter.write(objectToSave, converted); + client.replace(converted.getId(), converted.getExpiry(), converted.getRawValue()); + } + + public void update(Collection batchToSave) { + Iterator iter = batchToSave.iterator(); + while(iter.hasNext()) { + save(iter.next()); + } + } + + public T findById(String id, Class entityClass) { + String result = (String) client.get(id); + if(result == null) { + return null; + } + + ConvertedCouchbaseDocument converted = new ConvertedCouchbaseDocument(id, result); + return couchbaseConverter.read(entityClass, converted); + } + + /** + * Make sure the given object is not a iterable. + * + * @param o the object to verify. + */ + protected void ensureNotIterable(Object o) { + if (null != o) { + if (o.getClass().isArray() || ITERABLE_CLASSES.contains(o.getClass().getName())) { + throw new IllegalArgumentException("Cannot use a collection here."); + } + } + } + + private RuntimeException potentiallyConvertRuntimeException(RuntimeException ex) { + RuntimeException resolved = this.exceptionTranslator.translateExceptionIfPossible(ex); + return resolved == null ? ex : resolved; + } + + +} diff --git a/src/main/java/com/couchbase/spring/core/convert/AbstractCouchbaseConverter.java b/src/main/java/com/couchbase/spring/core/convert/AbstractCouchbaseConverter.java new file mode 100644 index 00000000..ecc69ae8 --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/convert/AbstractCouchbaseConverter.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.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; + +public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, + InitializingBean { + + protected final GenericConversionService conversionService; + protected EntityInstantiators instantiators = new EntityInstantiators(); + + public AbstractCouchbaseConverter( + GenericConversionService conversionService) { + this.conversionService = conversionService; + } + + public ConversionService getConversionService() { + return conversionService; + } + + @Override + public void afterPropertiesSet() { + + } + +} diff --git a/src/main/java/com/couchbase/spring/core/convert/CouchbaseConverter.java b/src/main/java/com/couchbase/spring/core/convert/CouchbaseConverter.java new file mode 100644 index 00000000..f0c06148 --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/convert/CouchbaseConverter.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core.convert; + +import com.couchbase.spring.core.mapping.ConvertedCouchbaseDocument; +import com.couchbase.spring.core.mapping.CouchbasePersistentEntity; +import com.couchbase.spring.core.mapping.CouchbasePersistentProperty; +import org.springframework.data.convert.EntityConverter; +import org.springframework.data.convert.EntityReader; + +public interface CouchbaseConverter extends + EntityConverter, + CouchbasePersistentProperty, Object, ConvertedCouchbaseDocument>, + CouchbaseWriter, + EntityReader { +} diff --git a/src/main/java/com/couchbase/spring/core/convert/CouchbaseWriter.java b/src/main/java/com/couchbase/spring/core/convert/CouchbaseWriter.java new file mode 100644 index 00000000..99b4fd8e --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/convert/CouchbaseWriter.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core.convert; + +import org.springframework.data.convert.EntityWriter; + +public interface CouchbaseWriter extends + EntityWriter { + +} \ No newline at end of file diff --git a/src/main/java/com/couchbase/spring/core/convert/MappingCouchbaseConverter.java b/src/main/java/com/couchbase/spring/core/convert/MappingCouchbaseConverter.java new file mode 100644 index 00000000..ce23e4af --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/convert/MappingCouchbaseConverter.java @@ -0,0 +1,233 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core.convert; + +import com.couchbase.spring.core.mapping.ConvertedCouchbaseDocument; +import com.couchbase.spring.core.mapping.CouchbasePersistentEntity; +import com.couchbase.spring.core.mapping.CouchbasePersistentProperty; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import org.codehaus.jackson.JsonEncoding; +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonGenerator; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.data.convert.EntityInstantiator; +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.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.util.Assert; + +public class MappingCouchbaseConverter extends AbstractCouchbaseConverter + implements ApplicationContextAware { + + protected ApplicationContext applicationContext; + protected final MappingContext, + CouchbasePersistentProperty> mappingContext; + protected boolean useFieldAccessOnly = true; + + @SuppressWarnings("deprecation") + public MappingCouchbaseConverter(MappingContext, + CouchbasePersistentProperty> mappingContext) { + super(ConversionServiceFactory.createDefaultConversionService()); + + this.mappingContext = mappingContext; + } + + @Override + public MappingContext, + CouchbasePersistentProperty> getMappingContext() { + 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; + } + + @Override + public void write(Object source, ConvertedCouchbaseDocument 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()); + } + + } + + 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()); + } + + final CouchbasePersistentProperty idProperty = entity.getIdProperty(); + if(idProperty == null) { + throw new MappingException("ID property required for entity of type " + + source.getClass().getName()); + } + + 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.writeStartObject(); + entity.doWithProperties(new PropertyHandler() { + @Override + public void doWithPersistentProperty(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()); + } + } + + } + }); + jsonGenerator.writeEndObject(); + jsonGenerator.close(); + + target.setRawValue(jsonStream.toString()); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } + + private class CouchbasePropertyValueProvider implements PropertyValueProvider { + + private final ConvertedCouchbaseDocument source; + private final Object parent; + + public CouchbasePropertyValueProvider(ConvertedCouchbaseDocument source, Object parent) { + Assert.notNull(source); + this.source = source; + this.parent = 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; + } + + return value; + } + } + +} diff --git a/src/main/java/com/couchbase/spring/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/com/couchbase/spring/core/mapping/BasicCouchbasePersistentEntity.java new file mode 100644 index 00000000..941d7d84 --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/mapping/BasicCouchbasePersistentEntity.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.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; + +public class BasicCouchbasePersistentEntity + extends BasicPersistentEntity + implements CouchbasePersistentEntity, ApplicationContextAware { + + private final StandardEvaluationContext context; + + public BasicCouchbasePersistentEntity(TypeInformation typeInformation) { + super(typeInformation); + + this.context = new StandardEvaluationContext(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + context.addPropertyAccessor(new BeanFactoryAccessor()); + context.setBeanResolver(new BeanFactoryResolver(applicationContext)); + context.setRootObject(applicationContext); + } + + public int getExpiry() { + com.couchbase.spring.core.mapping.Document annotation = + getType().getAnnotation(com.couchbase.spring.core.mapping.Document.class); + + if(annotation == null) { + return 0; + } + return annotation.expiry(); + } + +} diff --git a/src/main/java/com/couchbase/spring/core/mapping/BasicCouchbasePersistentProperty.java b/src/main/java/com/couchbase/spring/core/mapping/BasicCouchbasePersistentProperty.java new file mode 100644 index 00000000..b2db7d6b --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/mapping/BasicCouchbasePersistentProperty.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.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.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. + */ +public class BasicCouchbasePersistentProperty + extends AnnotationBasedPersistentProperty + implements CouchbasePersistentProperty { + + /** + * 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(Field field, + PropertyDescriptor propertyDescriptor, CouchbasePersistentEntity owner, + SimpleTypeHolder simpleTypeHolder) { + super(field, propertyDescriptor, owner, simpleTypeHolder); + } + + /** + * 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() { + com.couchbase.spring.core.mapping.Field annotation = getField(). + getAnnotation(com.couchbase.spring.core.mapping.Field.class); + + return annotation != null && StringUtils.hasText(annotation.value()) + ? annotation.value() : field.getName(); + } + +} diff --git a/src/main/java/com/couchbase/spring/core/mapping/ConvertedCouchbaseDocument.java b/src/main/java/com/couchbase/spring/core/mapping/ConvertedCouchbaseDocument.java new file mode 100644 index 00000000..4ff45218 --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/mapping/ConvertedCouchbaseDocument.java @@ -0,0 +1,105 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core.mapping; + +import java.util.HashMap; +import java.util.Map; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.TypeReference; +import org.springframework.data.mapping.model.MappingException; + +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/com/couchbase/spring/core/mapping/CouchbasePersistentEntity.java b/src/main/java/com/couchbase/spring/core/mapping/CouchbasePersistentEntity.java new file mode 100644 index 00000000..1d42d104 --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/mapping/CouchbasePersistentEntity.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core.mapping; + +import org.springframework.data.mapping.PersistentEntity; + +public interface CouchbasePersistentEntity extends + PersistentEntity { + + /** + * Returns the expiry time for the document. + * + * @return + */ + int getExpiry(); +} diff --git a/src/main/java/com/couchbase/spring/core/mapping/CouchbasePersistentProperty.java b/src/main/java/com/couchbase/spring/core/mapping/CouchbasePersistentProperty.java new file mode 100644 index 00000000..cc26e5a2 --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/mapping/CouchbasePersistentProperty.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core.mapping; + +import org.springframework.data.mapping.PersistentProperty; + + +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/com/couchbase/spring/core/mapping/Document.java b/src/main/java/com/couchbase/spring/core/mapping/Document.java new file mode 100644 index 00000000..69792ba9 --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/mapping/Document.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core.mapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.data.annotation.Persistent; + +/** + * Identifies a domain object to be persisted to Couchbase. + */ +@Persistent +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface Document { + + /** + * An optional expiry time for the document. + */ + int expiry() default 0; + +} diff --git a/src/main/java/com/couchbase/spring/core/mapping/Field.java b/src/main/java/com/couchbase/spring/core/mapping/Field.java new file mode 100644 index 00000000..8fe39a49 --- /dev/null +++ b/src/main/java/com/couchbase/spring/core/mapping/Field.java @@ -0,0 +1,23 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.couchbase.spring.core.mapping; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotation to define custom metadata for document fields. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface Field { + + /** + * The key to be used to store the field inside the document. + */ + String value() default ""; + +} diff --git a/src/test/java/com/couchbase/spring/config/TestApplicationConfig.java b/src/test/java/com/couchbase/spring/TestApplicationConfig.java similarity index 91% rename from src/test/java/com/couchbase/spring/config/TestApplicationConfig.java rename to src/test/java/com/couchbase/spring/TestApplicationConfig.java index e9165232..4aba2b8c 100644 --- a/src/test/java/com/couchbase/spring/config/TestApplicationConfig.java +++ b/src/test/java/com/couchbase/spring/TestApplicationConfig.java @@ -20,9 +20,10 @@ * IN THE SOFTWARE. */ -package com.couchbase.spring.config; +package com.couchbase.spring; import com.couchbase.client.CouchbaseClient; +import com.couchbase.spring.config.AbstractCouchbaseConfiguration; import java.io.IOException; import java.net.URI; import java.util.Arrays; @@ -32,12 +33,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; @Configuration -public class TestApplicationConfig { +public class TestApplicationConfig extends AbstractCouchbaseConfiguration { @Autowired private Environment env; @Bean + @Override public CouchbaseClient couchbaseClient() throws IOException { String defaultHost = "http://127.0.0.1:8091/pools"; String host = env.getProperty("couchbase.host", defaultHost); @@ -46,4 +48,5 @@ public class TestApplicationConfig { String pass = env.getProperty("couchbase.password", ""); return new CouchbaseClient(Arrays.asList(URI.create(host)), bucket, pass); } + } diff --git a/src/test/java/com/couchbase/spring/cache/CouchbaseCacheManagerTest.java b/src/test/java/com/couchbase/spring/cache/CouchbaseCacheManagerTest.java index e71ecea9..da538f87 100644 --- a/src/test/java/com/couchbase/spring/cache/CouchbaseCacheManagerTest.java +++ b/src/test/java/com/couchbase/spring/cache/CouchbaseCacheManagerTest.java @@ -23,7 +23,7 @@ package com.couchbase.spring.cache; import com.couchbase.client.CouchbaseClient; -import com.couchbase.spring.config.TestApplicationConfig; +import com.couchbase.spring.TestApplicationConfig; import java.util.HashMap; import static org.junit.Assert.*; import org.junit.Test; diff --git a/src/test/java/com/couchbase/spring/cache/CouchbaseCacheTest.java b/src/test/java/com/couchbase/spring/cache/CouchbaseCacheTest.java index 6e09ea1a..1e149c8f 100644 --- a/src/test/java/com/couchbase/spring/cache/CouchbaseCacheTest.java +++ b/src/test/java/com/couchbase/spring/cache/CouchbaseCacheTest.java @@ -23,7 +23,7 @@ package com.couchbase.spring.cache; import com.couchbase.client.CouchbaseClient; -import com.couchbase.spring.config.TestApplicationConfig; +import com.couchbase.spring.TestApplicationConfig; import static org.junit.Assert.*; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/src/test/java/com/couchbase/spring/config/AbstractCouchbaseConfigurationTest.java b/src/test/java/com/couchbase/spring/config/AbstractCouchbaseConfigurationTest.java new file mode 100644 index 00000000..2604755b --- /dev/null +++ b/src/test/java/com/couchbase/spring/config/AbstractCouchbaseConfigurationTest.java @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.config; + +import com.couchbase.client.CouchbaseClient; +import com.couchbase.spring.TestApplicationConfig; +import com.couchbase.spring.core.mapping.Document; +import static org.junit.Assert.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Unit test for {@link AbstractCouchbaseConfiguration} + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestApplicationConfig.class) +public class AbstractCouchbaseConfigurationTest { + + @Autowired + private CouchbaseClient client; + + @Test + public void usesConfigClassPackageAsBaseMappingPackage() throws Exception { + AbstractCouchbaseConfiguration config = new SampleCouchbaseConfiguration(); + + assertEquals(config.getMappingBasePackage(), + SampleCouchbaseConfiguration.class.getPackage().getName()); + assertEquals(config.getInitialEntitySet().size(), 1); + assertTrue(config.getInitialEntitySet().contains(Entity.class)); + } + + class SampleCouchbaseConfiguration extends AbstractCouchbaseConfiguration { + @Bean + @Override + public CouchbaseClient couchbaseClient() throws Exception { + return client; + } + } + + @Document + static class Entity { + } +} diff --git a/src/test/java/com/couchbase/spring/core/Beer.java b/src/test/java/com/couchbase/spring/core/Beer.java new file mode 100644 index 00000000..44a9d5ad --- /dev/null +++ b/src/test/java/com/couchbase/spring/core/Beer.java @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core; + +import org.springframework.data.annotation.Id; + +import com.couchbase.spring.core.mapping.Field; + +/** + * Test class for persisting and loading from {@link CouchbaseTemplate}. + */ +public class Beer { + + @Id + private final String id; + + private String name; + + @Field("is_active") + private boolean active = true; + + public Beer(String id) { + this.id = id; + } + + @Override + public String toString() { + return "Beer [id=" + id + ", name=" + name + ", active=" + active + "]"; + } + + public Beer setName(String name) { + this.name = name; + return this; + } + + public String getName() { + return name; + } + + public Beer setActive(boolean active) { + this.active = active; + return this; + } + + public boolean getActive() { + return active; + } + + public String getId() { + return id; + } + +} diff --git a/src/test/java/com/couchbase/spring/core/CouchbaseTemplateTest.java b/src/test/java/com/couchbase/spring/core/CouchbaseTemplateTest.java new file mode 100644 index 00000000..e4e54994 --- /dev/null +++ b/src/test/java/com/couchbase/spring/core/CouchbaseTemplateTest.java @@ -0,0 +1,140 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core; + +import com.couchbase.client.CouchbaseClient; +import com.couchbase.spring.TestApplicationConfig; +import com.couchbase.spring.core.mapping.Document; +import com.couchbase.spring.core.mapping.Field; + +import static org.junit.Assert.*; +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.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestApplicationConfig.class) +public class CouchbaseTemplateTest { + + @Autowired + private CouchbaseClient client; + + @Autowired + private CouchbaseTemplate template; + + @Test + public void saveSimpleEntityCorrectly() throws Exception { + String id = "beers:awesome-stout"; + String name = "The Awesome Stout"; + boolean active = false; + Beer beer = new Beer(id).setName(name).setActive(active); + + template.save(beer); + String result = (String) client.get(id); + + String expected = "{\"is_active\":" + active + ",\"name\":\"" + name + "\"}"; + assertNotNull(result); + assertEquals(expected, result); + } + + @Test + public void saveDocumentWithExpiry() throws Exception { + String id = "simple-doc-with-expiry"; + DocumentWithExpiry doc = new DocumentWithExpiry(id); + template.save(doc); + assertNotNull(client.get(id)); + Thread.sleep(3000); + assertNull(client.get(id)); + } + + @Test + public void insertDoesNotOverride() { + String id ="double-insert-test"; + String expected = "{\"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); + } + + @Test + public void updateDoesNotInsert() { + String id ="update-does-not-insert"; + SimplePerson doc = new SimplePerson(id, "Nice Guy"); + 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()); + } + + /** + * A sample document with just an id and property. + */ + @Document + class SimplePerson { + @Id + private final String id; + @Field + private final String name; + + public SimplePerson(String id, String name) { + this.id = id; + this.name = name; + } + } + + /** + * A sample document that expires in 2 seconds. + */ + @Document(expiry=2) + class DocumentWithExpiry { + @Id + private final String id; + + public DocumentWithExpiry(String id) { + this.id = id; + } + } +} diff --git a/src/test/java/com/couchbase/spring/core/mapping/BasicCouchbasePersistentPropertyTest.java b/src/test/java/com/couchbase/spring/core/mapping/BasicCouchbasePersistentPropertyTest.java new file mode 100644 index 00000000..6766b9f2 --- /dev/null +++ b/src/test/java/com/couchbase/spring/core/mapping/BasicCouchbasePersistentPropertyTest.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2009-2012 Couchbase, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING + * IN THE SOFTWARE. + */ + +package com.couchbase.spring.core.mapping; + +import java.lang.reflect.Field; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.util.ReflectionUtils; + +/** + * Verifies the correct behavior of properties on persistable objects. + */ +public class BasicCouchbasePersistentPropertyTest { + + /** + * Holds the entity to test against (contains the properties). + */ + CouchbasePersistentEntity entity; + + /** + * Create an instance of the demo entity. + */ + @Before + public void setUp() { + entity = new BasicCouchbasePersistentEntity( + ClassTypeInformation.from(Beer.class)); + } + + /** + * Verifies the name of the property without annotations. + */ + @Test + public void usesPropertyFieldName() { + Field field = ReflectionUtils.findField(Beer.class, "description"); + assertEquals("description", getPropertyFor(field).getFieldName()); + } + + /** + * Verifies the name of the property with custom name annotation. + */ + @Test + public void usesAnnotatedFieldName() { + Field field = ReflectionUtils.findField(Beer.class, "name"); + assertEquals("foobar", getPropertyFor(field).getFieldName()); + } + + /** + * Helper method to create a property out of the field. + * + * @param field the field to retrieve the properties from. + * @return the actual BasicCouchbasePersistentProperty instance. + */ + private CouchbasePersistentProperty getPropertyFor(Field field) { + return new BasicCouchbasePersistentProperty(field, null, entity, + new SimpleTypeHolder()); + } + + /** + * Simple POJO to test attribute properties and annotations. + */ + public class Beer { + + @Id + private String id; + + @com.couchbase.spring.core.mapping.Field("foobar") + String name; + + String description; + + } +}