From 0afeb0bf87c0d715534630a9ed3d69fbcd0a19cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 29 Jun 2015 16:44:10 +0200 Subject: [PATCH] implement template using SDK 2.x + tests --- .../core/CouchbaseTemplateTests.java | 555 +++++++++++++++ .../core/CouchbaseTemplateViewListener.java | 58 ++ .../AbstractCouchbaseConfiguration.java | 65 +- .../data/couchbase/core/BucketCallback.java | 39 + .../couchbase/core/CouchbaseOperations.java | 264 +++++++ .../couchbase/core/CouchbaseTemplate.java | 333 ++++++++- .../core/convert/ConverterRegistration.java | 6 +- .../JacksonTranslationService.java | 4 +- .../translation/TranslationService.java | 8 +- .../core/mapping/CouchbaseSimpleTypes.java | 7 +- .../data/couchbase/TestApplicationConfig.java | 67 ++ .../data/couchbase/core/Beer.java | 69 ++ .../JacksonTranslationServiceTests.java | 55 ++ ...BasicCouchbasePersistentPropertyTests.java | 99 +++ .../core/mapping/CustomConvertersTests.java | 186 +++++ .../MappingCouchbaseConverterTests.java | 664 ++++++++++++++++++ 16 files changed, 2450 insertions(+), 29 deletions(-) create mode 100644 src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java create mode 100644 src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateViewListener.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/BucketCallback.java create mode 100644 src/test/java/org/springframework/data/couchbase/TestApplicationConfig.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/Beer.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java diff --git a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java new file mode 100644 index 00000000..1d2515df --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java @@ -0,0 +1,555 @@ +/* + * 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; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.document.RawJsonDocument; +import com.couchbase.client.java.error.DocumentDoesNotExistException; +import com.couchbase.client.java.view.Stale; +import com.couchbase.client.java.view.ViewQuery; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.couchbase.TestApplicationConfig; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.Field; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Michael Nitschinger + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestApplicationConfig.class) +@TestExecutionListeners(CouchbaseTemplateViewListener.class) +public class CouchbaseTemplateTests { + + @Autowired + private Bucket client; + + @Autowired + private CouchbaseTemplate template; + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private void removeIfExist(String key) { + try { + client.remove(key); + } + catch (DocumentDoesNotExistException e) { + //ignore + } + } + + @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); + RawJsonDocument resultDoc = client.get(id, RawJsonDocument.class); + assertNotNull(resultDoc); + String result = resultDoc.content(); + assertNotNull(result); + Map resultConv = MAPPER.readValue(result, new TypeReference>() {}); + + assertEquals("org.springframework.data.couchbase.core.Beer", resultConv.get("_class")); + assertEquals(false, resultConv.get("is_active")); + assertEquals("The Awesome Stout", resultConv.get("name")); + } + + @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() throws Exception { + String id = "double-insert-test"; + removeIfExist(id); + + SimplePerson doc = new SimplePerson(id, "Mr. A"); + template.insert(doc); + RawJsonDocument resultDoc = client.get(id, RawJsonDocument.class); + assertNotNull(resultDoc); + String result = resultDoc.content(); + + Map resultConv = MAPPER.readValue(result, new TypeReference>() {}); + assertEquals("Mr. A", resultConv.get("name")); + + doc = new SimplePerson(id, "Mr. B"); + template.insert(doc); + + resultDoc = client.get(id, RawJsonDocument.class); + assertNotNull(resultDoc); + result = resultDoc.content(); + + resultConv = MAPPER.readValue(result, new TypeReference>() {}); + assertEquals("Mr. A", resultConv.get("name")); + } + + + @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 removeDocument() { + String id = "beers:to-delete-stout"; + Beer beer = new Beer(id); + + template.save(beer); + Object result = client.get(id); + assertNotNull(result); + + template.remove(beer); + result = client.get(id); + assertNull(result); + } + + + @Test + public void storeListsAndMaps() { + String id = "persons:lots-of-names"; + List names = new ArrayList(); + names.add("Michael"); + names.add("Thomas"); + names.add(null); + List votes = new LinkedList(); + Map info1 = new HashMap(); + info1.put("foo", true); + info1.put("bar", false); + info1.put("nullValue", null); + Map info2 = new HashMap(); + + ComplexPerson complex = new ComplexPerson(id, names, votes, info1, info2); + + template.save(complex); + assertNotNull(client.get(id)); + + ComplexPerson response = template.findById(id, ComplexPerson.class); + assertEquals(names, response.getFirstnames()); + assertEquals(votes, response.getVotes()); + assertEquals(id, response.getId()); + 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()); + } + + @Test + public void shouldLoadAndMapViewDocs() { + ViewQuery query = ViewQuery.from("test_beers", "by_name"); + query.stale(Stale.FALSE); + + final List beers = template.findByView(query, Beer.class); + assertTrue(beers.size() > 0); + + for (Beer beer : beers) { + assertNotNull(beer.getId()); + assertNotNull(beer.getName()); + assertNotNull(beer.getActive()); + } + } + + @Test + public void shouldDeserialiseLongs() { + final long time = new Date().getTime(); + SimpleWithLong simpleWithLong = new SimpleWithLong("simpleWithLong:simple", time); + template.save(simpleWithLong); + simpleWithLong = template.findById("simpleWithLong:simple", SimpleWithLong.class); + assertNotNull(simpleWithLong); + assertEquals(time, simpleWithLong.getValue()); + } + + @Test + public void shouldDeserialiseEnums() { + SimpleWithEnum simpleWithEnum = new SimpleWithEnum("simpleWithEnum:enum", SimpleWithEnum.Type.BIG); + template.save(simpleWithEnum); + simpleWithEnum = template.findById("simpleWithEnum:enum", SimpleWithEnum.class); + assertNotNull(simpleWithEnum); + assertEquals(simpleWithEnum.getType(), SimpleWithEnum.Type.BIG); + } + + @Test + public void shouldDeserialiseClass() { + SimpleWithClass simpleWithClass = new SimpleWithClass("simpleWithClass:class", Integer.class); + simpleWithClass.setValue("The dish ran away with the spoon."); + template.save(simpleWithClass); + simpleWithClass = template.findById("simpleWithClass:class", SimpleWithClass.class); + assertNotNull(simpleWithClass); + assertThat(simpleWithClass.getValue(), equalTo("The dish ran away with the spoon.")); + } + + @Test + public void shouldHandleCASVersionOnInsert() throws Exception { + removeIfExist("versionedClass:1"); + + VersionedClass versionedClass = new VersionedClass("versionedClass:1", "foobar"); + assertEquals(0, versionedClass.getVersion()); + template.insert(versionedClass); + RawJsonDocument rawStored = client.get("versionedClass:1", RawJsonDocument.class); + assertEquals(rawStored.cas(), versionedClass.getVersion()); + } + + @Test + public void versionShouldNotUpdateOnSecondInsert() throws Exception { + removeIfExist("versionedClass:2"); + + VersionedClass versionedClass = new VersionedClass("versionedClass:2", "foobar"); + template.insert(versionedClass); + long version1 = versionedClass.getVersion(); + template.insert(versionedClass); + long version2 = versionedClass.getVersion(); + + assertTrue(version1 > 0); + assertTrue(version2 > 0); + assertEquals(version1, version2); + } + + @Test + public void shouldSaveDocumentOnMatchingVersion() throws Exception { + removeIfExist("versionedClass:3"); + + VersionedClass versionedClass = new VersionedClass("versionedClass:3", "foobar"); + template.insert(versionedClass); + long version1 = versionedClass.getVersion(); + + versionedClass.setField("foobar2"); + template.save(versionedClass); + long version2 = versionedClass.getVersion(); + + assertTrue(version1 > 0); + assertTrue(version2 > 0); + assertNotEquals(version1, version2); + + assertEquals("foobar2", template.findById("versionedClass:3", VersionedClass.class).getField()); + } + + @Test(expected = OptimisticLockingFailureException.class) + public void shouldNotSaveDocumentOnNotMatchingVersion() throws Exception { + removeIfExist("versionedClass:4"); + + VersionedClass versionedClass = new VersionedClass("versionedClass:4", "foobar"); + template.insert(versionedClass); + + RawJsonDocument toCompare = RawJsonDocument.create("versionedClass:4", "different"); + assertNotNull(client.upsert(toCompare)); + + versionedClass.setField("foobar2"); + //save (aka upsert) won't error in case of CAS mismatch anymore + template.update(versionedClass); + } + + @Test + public void shouldUpdateDocumentOnMatchingVersion() throws Exception { + removeIfExist("versionedClass:5"); + + VersionedClass versionedClass = new VersionedClass("versionedClass:5", "foobar"); + template.insert(versionedClass); + long version1 = versionedClass.getVersion(); + + versionedClass.setField("foobar2"); + template.update(versionedClass); + long version2 = versionedClass.getVersion(); + + assertTrue(version1 > 0); + assertTrue(version2 > 0); + assertNotEquals(version1, version2); + + assertEquals("foobar2", template.findById("versionedClass:5", VersionedClass.class).getField()); + } + + @Test(expected = OptimisticLockingFailureException.class) + public void shouldNotUpdateDocumentOnNotMatchingVersion() throws Exception { + removeIfExist("versionedClass:6"); + + VersionedClass versionedClass = new VersionedClass("versionedClass:6", "foobar"); + template.insert(versionedClass); + + RawJsonDocument toCompare = RawJsonDocument.create("versionedClass:6", "different"); + assertNotNull(client.upsert(toCompare)); + + versionedClass.setField("foobar2"); + template.update(versionedClass); + } + + @Test + public void shouldLoadVersionPropertyOnFind() throws Exception { + removeIfExist("versionedClass:7"); + + VersionedClass versionedClass = new VersionedClass("versionedClass:7", "foobar"); + template.insert(versionedClass); + assertTrue(versionedClass.getVersion() > 0); + + VersionedClass foundClass = template.findById("versionedClass:7", VersionedClass.class); + assertEquals(versionedClass.getVersion(), foundClass.getVersion()); + } + + /** + * A sample document with just an id and property. + */ + @Document + static 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) + static class DocumentWithExpiry { + + @Id + private final String id; + + public DocumentWithExpiry(String id) { + this.id = id; + } + } + + @Document + static class ComplexPerson { + + @Id + private final String id; + @Field + private final List firstnames; + @Field + private final List votes; + + @Field + private final Map info1; + @Field + private final Map info2; + + public ComplexPerson(String id, List firstnames, + List votes, Map info1, + Map info2) { + this.id = id; + this.firstnames = firstnames; + this.votes = votes; + this.info1 = info1; + this.info2 = info2; + } + + List getFirstnames() { + return firstnames; + } + + List getVotes() { + return votes; + } + + Map getInfo1() { + return info1; + } + + Map getInfo2() { + return info2; + } + + String getId() { + return id; + } + } + + @Document + static class SimpleWithLong { + + @Id + private String id; + + private long value; + + SimpleWithLong(final String id, final long value) { + this.id = id; + this.value = value; + } + + String getId() { + return id; + } + + long getValue() { + return value; + } + + void setValue(final long value) { + this.value = value; + } + } + + static class SimpleWithEnum { + + @Id + private String id; + + private enum Type { + BIG + } + + private Type type; + + SimpleWithEnum(final String id, final Type type) { + this.id = id; + this.type = type; + } + + String getId() { + return id; + } + + void setId(final String id) { + this.id = id; + } + + Type getType() { + return type; + } + + void setType(final Type type) { + this.type = type; + } + } + + static class SimpleWithClass { + + @Id + private String id; + + private Class integerClass; + + private String value; + + SimpleWithClass(final String id, final Class integerClass) { + this.id = id; + this.integerClass = integerClass; + } + + String getId() { + return id; + } + + void setId(final String id) { + this.id = id; + } + + Class getIntegerClass() { + return integerClass; + } + + void setIntegerClass(final Class integerClass) { + this.integerClass = integerClass; + } + + String getValue() { + return value; + } + + void setValue(final String value) { + this.value = value; + } + } + + static class VersionedClass { + + @Id + private String id; + + @Version + private long version; + + private String field; + + VersionedClass(String id, String field) { + this.id = id; + this.field = field; + } + + public String getId() { + return id; + } + + public long getVersion() { + return version; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + } + +} diff --git a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateViewListener.java b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateViewListener.java new file mode 100644 index 00000000..07900d62 --- /dev/null +++ b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateViewListener.java @@ -0,0 +1,58 @@ +/* + * 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; + +import java.util.Collections; + +import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.view.DefaultView; +import com.couchbase.client.java.view.DesignDocument; +import com.couchbase.client.java.view.View; + +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; + +/** + * @author Michael Nitschinger + */ +public class CouchbaseTemplateViewListener extends DependencyInjectionTestExecutionListener { + + @Override + public void beforeTestClass(final TestContext testContext) throws Exception { + Bucket client = (Bucket) testContext.getApplicationContext().getBean("couchbaseBucket"); + populateTestData(client); + createAndWaitForDesignDocs(client); + } + + private void populateTestData(Bucket client) { + CouchbaseTemplate template = new CouchbaseTemplate(client); + + for (int i = 0; i < 100; i++) { + Beer b = new Beer("testbeer-" + i).setName("MyBeer " + i).setActive(true); + template.save(b); + } + } + + private void createAndWaitForDesignDocs(Bucket client) { + String mapFunction = "function (doc, meta) { if(doc._class == " + + "\"org.springframework.data.couchbase.core.Beer\") { emit(doc.name, null); } }"; + View view = DefaultView.create("by_name", mapFunction); + DesignDocument designDoc = DesignDocument.create("test_beers", Collections.singletonList(view)); + client.bucketManager().upsertDesignDocument(designDoc); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 5d9829e2..4095c355 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.data.couchbase.config; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -29,12 +30,15 @@ import com.couchbase.client.java.env.DefaultCouchbaseEnvironment; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; -import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.ConfigurationCondition; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.data.annotation.Persistent; import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.convert.CustomConversions; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; +import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; import org.springframework.data.couchbase.core.mapping.Document; import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy; import org.springframework.data.mapping.model.FieldNamingStrategy; @@ -130,17 +134,63 @@ public abstract class AbstractCouchbaseConfiguration { */ @Bean(name = BeanNames.COUCHBASE_TEMPLATE) public CouchbaseTemplate couchbaseTemplate() throws Exception { - //TODO use mappingCouchbaseConverter and translationService when implemented - return new CouchbaseTemplate(couchbaseClient()); + return new CouchbaseTemplate(couchbaseClient(), mappingCouchbaseConverter(), translationService()); } - //TODO create beans for mappingCouchbaseConverter, translationService, couchbaseMappingContext when implemented - //TODO for mappingCouchbaseConverter, allow registering of customConversions + /** + * Creates a {@link MappingCouchbaseConverter} using the configured {@link #couchbaseMappingContext}. + * + * @throws Exception on Bean construction failure. + */ + @Bean + public MappingCouchbaseConverter mappingCouchbaseConverter() throws Exception { + MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext()); + converter.setCustomConversions(customConversions()); + return converter; + } + + /** + * Creates a {@link TranslationService}. + * + * @return TranslationService, defaulting to JacksonTranslationService. + */ + @Bean + public TranslationService translationService() { + final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService(); + jacksonTranslationService.afterPropertiesSet(); + return jacksonTranslationService; + } + + /** + * Creates a {@link CouchbaseMappingContext} equipped with entity classes scanned from the mapping base package. + * + * @throws Exception on Bean construction failure. + */ + @Bean + public CouchbaseMappingContext couchbaseMappingContext() throws Exception { + CouchbaseMappingContext mappingContext = new CouchbaseMappingContext(); + mappingContext.setInitialEntitySet(getInitialEntitySet()); + mappingContext.setSimpleTypeHolder(customConversions().getSimpleTypeHolder()); + mappingContext.setFieldNamingStrategy(fieldNamingStrategy()); + return mappingContext; + } + + /** + * Register custom Converters in a {@link CustomConversions} object if required. These + * {@link CustomConversions} will be registered with the {@link #mappingCouchbaseConverter()} and + * {@link #couchbaseMappingContext()}. Returns an empty {@link CustomConversions} instance by default. + * + * @return must not be {@literal null}. + */ + @Bean + public CustomConversions customConversions() { + return new CustomConversions(Collections.emptyList()); + } /** * Scans the mapping base package for classes annotated with {@link Document}. * - * @throws ClassNotFoundException if initial entity sets could not be loaded. + * @throws ClassNotFoundException if intial entity sets could not be loaded. */ protected Set> getInitialEntitySet() throws ClassNotFoundException { String basePackage = getMappingBasePackage(); @@ -187,7 +237,6 @@ public abstract class AbstractCouchbaseConfiguration { * @return the naming strategy. */ protected FieldNamingStrategy fieldNamingStrategy() { - //TODO implement a CouchbaseMappingContext, use this method (update link in javadoc) return abbreviateFieldNames() ? new CamelCaseAbbreviatingFieldNamingStrategy() : PropertyNameFieldNamingStrategy.INSTANCE; } } diff --git a/src/main/java/org/springframework/data/couchbase/core/BucketCallback.java b/src/main/java/org/springframework/data/couchbase/core/BucketCallback.java new file mode 100644 index 00000000..878707f8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/BucketCallback.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2015 the original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.couchbase.core; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +/** + * Defines the callback which will be wrapped and executed on a bucket. + * + * @author Michael Nitschinger + */ +public interface BucketCallback { + + /** + * The enclosed body will be executed on the connected bucket. + * + * @return the result of the enclosed execution. + * @throws TimeoutException if the enclosed operation timed out. + * @throws ExecutionException if the result could not be retrieved because of a thrown exception before. + * @throws InterruptedException if the enclosed operation was interrupted. + */ + T doInBucket() throws TimeoutException, ExecutionException, InterruptedException; + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java index c403ebbd..b6675bce 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseOperations.java @@ -17,7 +17,18 @@ package org.springframework.data.couchbase.core; +import java.util.Collection; +import java.util.List; + import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.PersistTo; +import com.couchbase.client.java.ReplicateTo; +import com.couchbase.client.java.query.Query; +import com.couchbase.client.java.query.QueryParams; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.query.Statement; +import com.couchbase.client.java.view.ViewQuery; +import com.couchbase.client.java.view.ViewResult; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; @@ -29,6 +40,259 @@ import org.springframework.data.couchbase.core.convert.CouchbaseConverter; */ 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.

+ * + * @param objectToSave the object to store in the bucket. + */ + void save(Object objectToSave); + + /** + * Save the given object. + *

+ *

When the document already exists (specified by its unique id), then it will be overriden. Otherwise it will be + * created.

+ * + * @param objectToSave the object to store in the bucket. + */ + void save(Object objectToSave, PersistTo persistTo, ReplicateTo replicateTo); + + /** + * 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); + + /** + * 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. + * @param persistTo the persistence constraint setting. + * @param replicateTo the replication constraint setting. + */ + void save(Collection batchToSave, PersistTo persistTo, ReplicateTo replicateTo); + + /** + * 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 task.

+ * + * @param objectToInsert the object to add to the bucket. + */ + void insert(Object objectToInsert); + + /** + * 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 task.

+ * + * @param objectToInsert the object to add to the bucket. + * @param persistTo the persistence constraint setting. + * @param replicateTo the replication constraint setting. + */ + void insert(Object objectToInsert, PersistTo persistTo, ReplicateTo replicateTo); + + /** + * 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 batchToInsert the list of objects to add to the bucket. + */ + void insert(Collection batchToInsert); + + /** + * 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 batchToInsert the list of objects to add to the bucket. + * @param persistTo the persistence constraint setting. + * @param replicateTo the replication constraint setting. + */ + void insert(Collection batchToInsert, PersistTo persistTo, ReplicateTo replicateTo); + + /** + * Update the given object. + *

+ *

When the document does not exist (specified by its unique id) it will not be created. Use the + * {@link CouchbaseOperations#save} method for this.

+ * + * @param objectToUpdate the object to add to the bucket. + */ + void update(Object objectToUpdate); + + /** + * Update the given object. + *

+ *

When the document does not exist (specified by its unique id) it will not be created. Use the + * {@link CouchbaseOperations#save} method for this.

+ * + * @param objectToUpdate the object to add to the bucket. + * @param persistTo the persistence constraint setting. + * @param replicateTo the replication constraint setting. + */ + void update(Object objectToUpdate, PersistTo persistTo, ReplicateTo replicateTo); + + /** + * Insert a list of objects. + *

+ *

If one of the documents does not exist (specified by its unique id), then it will not be created. Use the + * {@link CouchbaseOperations#save} method for this.

+ * + * @param batchToUpdate the list of objects to add to the bucket. + */ + void update(Collection batchToUpdate); + + /** + * Insert a list of objects. + *

+ *

If one of the documents does not exist (specified by its unique id), then it will not be created. Use the + * {@link CouchbaseOperations#save} method for this.

+ * + * @param batchToUpdate the list of objects to add to the bucket. + * @param persistTo the persistence constraint setting. + * @param replicateTo the replication constraint setting. + */ + void update(Collection batchToUpdate, PersistTo persistTo, ReplicateTo replicateTo); + + /** + * 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. + */ + T findById(String id, Class entityClass); + + //TODO add javadoc link to setIncludeDocs when GA + + /** + * Query a View for a list of documents of type T. + *

+ *

There is no need to setIncludeDocs(boolean) explicitly, because it will be set to true all the + * time. It is valid to pass in a empty constructed {@link ViewQuery} object.

+ *

+ *

This method does not work with reduced views, because they by design do not contain references to original + * objects. Use the provided {@link #queryView} method for more flexibility and direct access.

+ * + * @param query the Query object (also specifying view design document and view name). + * @param entityClass the entity to map to. + * @return the converted collection + */ + List findByView(ViewQuery query, Class entityClass); + + + /** + * Query a View with direct access to the {@link ViewResult}. + *

This method is available to ease the working with views by still wrapping exceptions into the Spring + * infrastructure.

+ *

It is especially needed if you want to run reduced viewName queries, because they can't be mapped onto entities + * directly.

+ * + * @param query the Query object (also specifying view design document and view name). + * @return ViewResult containing the results of the query. + */ + ViewResult queryView(ViewQuery query); + + /** + * Query the N1QL Service for JSON data of type T. This is done via a {@link Query} that can + * contain a {@link Statement}, additional query parameters ({@link QueryParams}) + * and placeholder values if the statement contains placeholders. + *

Use {@link Query}'s factory methods to construct this.

+ * + * @param n1ql the N1QL query. + * @param entityClass the target class for the returned entities. + * @param the entity class + * @return the list of entities matching this query. + */ + List findByN1QL(Query n1ql, Class entityClass); + + /** + * Query the N1QL Service with direct access to the {@link QueryResult}. + *

+ * This is done via a {@link Query} that can + * contain a {@link Statement}, additional query parameters ({@link QueryParams}) + * and placeholder values if the statement contains placeholders.

+ *

+ * Use {@link Query}'s factory methods to construct this.

+ * + * @param n1ql the N1QL query. + * @return {@link QueryResult} containing the results of the n1ql query. + */ + QueryResult queryN1QL(Query n1ql); + + /** + * Checks if the given document exists. + * + * @param id the unique ID of the document. + * @return whether the document could be found or not. + */ + boolean exists(String id); + + /** + * Remove the given object from the bucket by id. + *

+ * If the object is a String, it will be treated as the document key + * directly. + * + * @param objectToRemove the Object to remove. + */ + void remove(Object objectToRemove); + + /** + * Remove the given object from the bucket by id. + *

+ * If the object is a String, it will be treated as the document key + * directly. + * + * @param objectToRemove the Object to remove. + * @param persistTo the persistence constraint setting. + * @param replicateTo the replication constraint setting. + */ + void remove(Object objectToRemove, PersistTo persistTo, ReplicateTo replicateTo); + + /** + * Remove a list of objects from the bucket by id. + * + * @param batchToRemove the list of Objects to remove. + */ + void remove(Collection batchToRemove); + + /** + * Remove a list of objects from the bucket by id. + * + * @param batchToRemove the list of Objects to remove. + * @param persistTo the persistence constraint setting. + * @param replicateTo the replication constraint setting. + */ + void remove(Collection batchToRemove, PersistTo persistTo, ReplicateTo replicateTo); + + /** + * Executes a BucketCallback translating any exceptions as necessary. + *

+ * Allows for returning a result object, that is a domain object or a collection of domain objects. + * + * @param action the action to execute in the callback. + * @param the return type. + * @return the return type. + */ + T execute(BucketCallback action); + /** * Returns the linked {@link Bucket} to this template. * 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 9968028b..8f31c1fa 100644 --- a/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java +++ b/src/main/java/org/springframework/data/couchbase/core/CouchbaseTemplate.java @@ -16,25 +16,47 @@ package org.springframework.data.couchbase.core; +import java.util.ArrayList; 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 java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import com.couchbase.client.java.Bucket; +import com.couchbase.client.java.PersistTo; +import com.couchbase.client.java.ReplicateTo; +import com.couchbase.client.java.document.Document; +import com.couchbase.client.java.document.RawJsonDocument; +import com.couchbase.client.java.error.CASMismatchException; +import com.couchbase.client.java.query.Query; +import com.couchbase.client.java.query.QueryResult; +import com.couchbase.client.java.view.ViewQuery; +import com.couchbase.client.java.view.ViewResult; +import com.couchbase.client.java.view.ViewRow; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +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 org.springframework.data.mapping.model.BeanWrapper; /** * @author Michael Nitschinger @@ -46,6 +68,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseTemplate.class); private static final WriteResultChecking DEFAULT_WRITE_RESULT_CHECKING = WriteResultChecking.NONE; private static final Collection ITERABLE_CLASSES; + static { final Set iterableClasses = new HashSet(); iterableClasses.add(List.class.getName()); @@ -61,6 +84,9 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP private ApplicationEventPublisher eventPublisher; private WriteResultChecking writeResultChecking = DEFAULT_WRITE_RESULT_CHECKING; + private PersistenceExceptionTranslator exceptionTranslator = new CouchbaseExceptionTranslator(); + + protected final MappingContext, CouchbasePersistentProperty> mappingContext; public CouchbaseTemplate(final Bucket client) { this(client, null, null); @@ -75,6 +101,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP this.client = client; this.converter = converter == null ? getDefaultConverter() : converter; this.translationService = translationService == null ? getDefaultTranslationService() : translationService; + this.mappingContext = this.converter.getMappingContext(); } private TranslationService getDefaultTranslationService() { @@ -89,15 +116,27 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP return c; } - /** Encode a {@link CouchbaseStorable} into a storable representation (JSON) **/ - private Object translateEncode(final CouchbaseStorable source) { - return translationService.encode(source); + /** + * Encode a {@link CouchbaseDocument} into a storable representation (JSON) then prepare + * it for storage as a {@link Document}. + */ + private Document encodeAndWrap(final CouchbaseDocument source, Long version) { + String encodedContent = translationService.encode(source); + if (version == null) { + return RawJsonDocument.create(source.getId(), source.getExpiration(), encodedContent); + } + else { + return RawJsonDocument.create(source.getId(), source.getExpiration(), encodedContent, version); + } } - /** Decode a JSON string into a {@link CouchbaseStorable} **/ - private CouchbaseStorable translateDecode(final String source, final CouchbaseStorable target) { - return translationService.decode(source, target); + /** + * Decode a {@link Document Document<String>} containing a JSON string + * into a {@link CouchbaseStorable} + */ + private CouchbaseStorable decodeAndUnwrap(final Document source, final CouchbaseStorable target) { + return translationService.decode(source.content(), target); //TODO rework and check } /** @@ -118,15 +157,16 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP * * @param message the message to use. */ - private void handleWriteResultError(String message) { + private void handleWriteResultError(String message, Exception cause) { if (writeResultChecking == WriteResultChecking.NONE) { return; } if (writeResultChecking == WriteResultChecking.EXCEPTION) { - throw new CouchbaseDataIntegrityViolationException(message); - } else { - LOGGER.error(message); + throw new CouchbaseDataIntegrityViolationException(message, cause); + } + else { + LOGGER.error(message, cause); } } @@ -139,6 +179,279 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationEventP this.eventPublisher = eventPublisher; } + @Override + public void save(Object objectToSave) { + save(objectToSave, PersistTo.NONE, ReplicateTo.NONE); + } + + @Override + public void save(Object objectToSave, PersistTo persistTo, ReplicateTo replicateTo) { + doPersist(objectToSave, persistTo, replicateTo, false, false); + } + + @Override + public void save(Collection batchToSave) { + save(batchToSave, PersistTo.NONE, ReplicateTo.NONE); + } + + @Override + public void save(Collection batchToSave, PersistTo persistTo, ReplicateTo replicateTo) { + for (Object o : batchToSave) { + doPersist(o, persistTo, replicateTo, false, false); + } + } + + @Override + public void insert(Object objectToInsert) { + insert(objectToInsert, PersistTo.NONE, ReplicateTo.NONE); + } + + @Override + public void insert(Object objectToInsert, PersistTo persistTo, ReplicateTo replicateTo) { + doPersist(objectToInsert, persistTo, replicateTo, true, false); + } + + @Override + public void insert(Collection batchToInsert) { + insert(batchToInsert, PersistTo.NONE, ReplicateTo.NONE); + } + + @Override + public void insert(Collection batchToInsert, PersistTo persistTo, ReplicateTo replicateTo) { + for (Object o : batchToInsert) { + doPersist(o, persistTo, replicateTo, true, false); + } + } + + @Override + public void update(Object objectToUpdate) { + update(objectToUpdate, PersistTo.NONE, ReplicateTo.NONE); + } + + @Override + public void update(Object objectToUpdate, PersistTo persistTo, ReplicateTo replicateTo) { + doPersist(objectToUpdate, persistTo, replicateTo, false, true); + } + + @Override + public void update(Collection batchToUpdate) { + update(batchToUpdate, PersistTo.NONE, ReplicateTo.NONE); + } + + @Override + public void update(Collection batchToUpdate, PersistTo persistTo, ReplicateTo replicateTo) { + for (Object o : batchToUpdate) { + doPersist(o, persistTo, replicateTo, false, true); + } + } + + @Override + public T findById(final String id, Class entityClass) { + RawJsonDocument result = execute(new BucketCallback() { + @Override + public RawJsonDocument doInBucket() { + return client.get(id, RawJsonDocument.class); + } + }); + + return mapToEntity(id, result, entityClass); + } + + @Override + public List findByView(ViewQuery query, Class entityClass) { + query.includeDocs(false); + query.reduce(false); + + final ViewResult response = queryView(query); + List allRows = response.allRows(); + //TODO error handling + + final List result = new ArrayList(allRows.size()); + for (final ViewRow row : allRows) { + result.add(mapToEntity(row.id(), row.document(RawJsonDocument.class), entityClass)); + } + + return result; + } + + @Override + public ViewResult queryView(final ViewQuery query) { + return execute(new BucketCallback() { + @Override + public ViewResult doInBucket() { + return client.query(query); + } + }); + } + + @Override + public List findByN1QL(Query n1ql, Class entityClass) { + //TODO find a way of mapping content to T + //TODO error handling + throw new NotImplementedException(); + } + + @Override + public QueryResult queryN1QL(final Query query) { + return execute(new BucketCallback() { + @Override + public QueryResult doInBucket() throws TimeoutException, ExecutionException, InterruptedException { + return client.query(query); + } + }); + } + + @Override + public boolean exists(final String id) { + return execute(new BucketCallback() { + @Override + public Boolean doInBucket() throws TimeoutException, ExecutionException, InterruptedException { + return client.exists(id); + } + }); + } + + @Override + public void remove(Object objectToRemove) { + remove(objectToRemove, PersistTo.NONE, ReplicateTo.NONE); + } + + @Override + public void remove(Object objectToRemove, PersistTo persistTo, ReplicateTo replicateTo) { + doRemove(objectToRemove, persistTo, replicateTo); + } + + @Override + public void remove(Collection batchToRemove) { + remove(batchToRemove, PersistTo.NONE, ReplicateTo.NONE); + } + + @Override + public void remove(Collection batchToRemove, PersistTo persistTo, ReplicateTo replicateTo) { + for (Object o : batchToRemove) { + doRemove(o, persistTo, replicateTo); + } + } + + @Override + public T execute(BucketCallback action) { + try { + return action.doInBucket(); + } + catch (RuntimeException e) { + throw exceptionTranslator.translateExceptionIfPossible(e); + } + catch (TimeoutException e) { + throw new QueryTimeoutException(e.getMessage(), e); + } + catch (InterruptedException e) { + throw new OperationInterruptedException(e.getMessage(), e); + } + catch (ExecutionException e) { + throw new OperationInterruptedException(e.getMessage(), e); + } + } + + private void doPersist(Object objectToPersist, final PersistTo persistTo, final ReplicateTo replicateTo, + final boolean failOnExist, final boolean failOnMissing) { + ensureNotIterable(objectToPersist); + + final String operationDesc = failOnExist ? "Insert" : failOnMissing ? "Update" : "Upsert"; + + final BeanWrapper beanWrapper = BeanWrapper.create(objectToPersist, converter.getConversionService()); + CouchbasePersistentEntity persistentEntity = mappingContext.getPersistentEntity(objectToPersist.getClass()); + final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty(); + final Long version = versionProperty != null ? beanWrapper.getProperty(versionProperty, Long.class) : null; + + //TODO event beforeConvert + final CouchbaseDocument converted = new CouchbaseDocument(); + converter.write(objectToPersist, converted); + + //TODO event beforeSave + execute(new BucketCallback() { + @Override + public Boolean doInBucket() throws InterruptedException, ExecutionException { + Document doc = encodeAndWrap(converted, version); + Document storedDoc; + try { + if (!failOnExist && !failOnMissing) { + storedDoc = client.upsert(doc, persistTo, replicateTo); + } + else if (failOnMissing) { + storedDoc = client.replace(doc, persistTo, replicateTo); + } + else { + storedDoc = client.insert(doc, persistTo, replicateTo); + } + + if (storedDoc != null && storedDoc.cas() != 0) { + //inject new cas into the bean + beanWrapper.setProperty(versionProperty, storedDoc.cas()); + return true; + } + return false; + } + catch (CASMismatchException e) { + throw new OptimisticLockingFailureException(operationDesc + + " document with version value failed: " + version); + } + catch (Exception e) { + handleWriteResultError(operationDesc + " document failed: " + e.getMessage(), e); + return false; //this could be skipped if WriteResultChecking.EXCEPTION + } + } + }); + //TODO event afterSave + } + + private void doRemove(final Object objectToRemove, final PersistTo persistTo, final ReplicateTo replicateTo) { + ensureNotIterable(objectToRemove); + + //TODO event BeforeDelete + if (objectToRemove instanceof String) { + execute(new BucketCallback() { + @Override + public Boolean doInBucket() throws InterruptedException, ExecutionException { + RawJsonDocument deletedDoc = client.remove((String) objectToRemove, persistTo, replicateTo, + RawJsonDocument.class); + return deletedDoc != null; + } + }); + //TODO event afterDelete + return; + } + + final CouchbaseDocument converted = new CouchbaseDocument(); + converter.write(objectToRemove, converted); + + execute(new BucketCallback() { + @Override + public Boolean doInBucket() { + RawJsonDocument deletedDoc = client.remove(converted.getId(), persistTo, replicateTo + , RawJsonDocument.class); + return deletedDoc != null; + } + }); + //TODO event afterDelete + } + + private T mapToEntity(String id, Document data, Class entityClass) { + if (data == null) { + return null; + } + + final CouchbaseDocument converted = new CouchbaseDocument(id); + Object readEntity = converter.read(entityClass, (CouchbaseDocument) decodeAndUnwrap(data, converted)); + + final BeanWrapper beanWrapper = BeanWrapper.create(readEntity, converter.getConversionService()); + CouchbasePersistentEntity persistentEntity = mappingContext.getPersistentEntity(readEntity.getClass()); + if (persistentEntity.hasVersionProperty()) { + beanWrapper.setProperty(persistentEntity.getVersionProperty(), data.cas()); + } + + return (T) readEntity; + } + @Override public Bucket getCouchbaseBucket() { return this.client; diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java index 4a8b53c0..a44c5c55 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterRegistration.java @@ -86,7 +86,7 @@ class ConverterRegistration { } /** - * Returns whether the source type is a Mongo simple one. + * Returns whether the source type is a Couchbase simple one. * * @return */ @@ -95,7 +95,7 @@ class ConverterRegistration { } /** - * Returns whether the target type is a Mongo simple one. + * Returns whether the target type is a Couchbase simple one. * * @return */ @@ -104,7 +104,7 @@ class ConverterRegistration { } /** - * Returns whether the given type is a type that Mongo can handle basically. + * Returns whether the given type is a type that Couchbase can handle basically. * * @param type * @return 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 index e87d2f59..a178f679 100644 --- 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 @@ -64,7 +64,7 @@ public class JacksonTranslationService implements TranslationService, Initializi * @return the encoded JSON String. */ @Override - public final Object encode(final CouchbaseStorable source) { + public final String encode(final CouchbaseStorable source) { Writer writer = new StringWriter(); try { @@ -125,7 +125,7 @@ public class JacksonTranslationService implements TranslationService, Initializi * @return the decoded structure. */ @Override - public final CouchbaseStorable decode(final Object source, final CouchbaseStorable target) { + public final CouchbaseStorable decode(final String source, final CouchbaseStorable target) { try { JsonParser parser = factory.createParser((String) source); while (parser.nextToken() != null) { 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 index 89940054..00ca6271 100644 --- 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 @@ -27,12 +27,12 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseStorable; public interface TranslationService { /** - * Encodes a {@link CouchbaseDocument} into the target format. + * Encodes a JSON String into the target format. * - * @param source the source document to encode. + * @param source the source contents to encode. * @return the encoded document representation. */ - Object encode(CouchbaseStorable source); + String encode(CouchbaseStorable source); /** * Decodes the target format into a {@link CouchbaseDocument} @@ -41,5 +41,5 @@ public interface TranslationService { * @param target the target of the populated data. * @return a properly populated document to work with. */ - CouchbaseStorable decode(Object source, CouchbaseStorable target); + CouchbaseStorable decode(String source, CouchbaseStorable target); } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java index 559d8e64..db066c5a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java @@ -21,14 +21,17 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import com.couchbase.client.java.document.RawJsonDocument; +import com.couchbase.client.java.document.json.JsonArray; + import org.springframework.data.mapping.model.SimpleTypeHolder; public abstract class CouchbaseSimpleTypes { static { Set> simpleTypes = new HashSet>(); - simpleTypes.add(CouchbaseDocument.class); - simpleTypes.add(CouchbaseList.class); + simpleTypes.add(RawJsonDocument.class); + simpleTypes.add(JsonArray.class); COUCHBASE_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); } diff --git a/src/test/java/org/springframework/data/couchbase/TestApplicationConfig.java b/src/test/java/org/springframework/data/couchbase/TestApplicationConfig.java new file mode 100644 index 00000000..b524936f --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/TestApplicationConfig.java @@ -0,0 +1,67 @@ +package org.springframework.data.couchbase; + +import java.util.Collections; +import java.util.List; + +import com.couchbase.client.java.env.CouchbaseEnvironment; +import com.couchbase.client.java.env.DefaultCouchbaseEnvironment; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration; +import org.springframework.data.couchbase.core.CouchbaseTemplate; +import org.springframework.data.couchbase.core.WriteResultChecking; + +@Configuration +public class TestApplicationConfig extends AbstractCouchbaseConfiguration { + + @Autowired + private Environment springEnv; + + @Bean + public String couchbaseAdminUser() { + return springEnv.getProperty("couchbase.adminUser", "Administrator"); + } + + @Bean + public String couchbaseAdminPassword() { + return springEnv.getProperty("couchbase.adminUser", "password"); + } + + @Override + protected List getBootstrapHosts() { + return Collections.singletonList(springEnv.getProperty("couchbase.host", "127.0.0.1")); + } + + @Override + protected String getBucketName() { + return springEnv.getProperty("couchbase.bucket", "default"); + } + + @Override + protected String getBucketPassword() { + return springEnv.getProperty("couchbase.password", ""); + } + + + //TODO maybe create the bucket if doesn't exist + + @Override + protected CouchbaseEnvironment getEnvironment() { + return DefaultCouchbaseEnvironment.builder() + .connectTimeout(10000) + .kvTimeout(10000) + .queryTimeout(10000) + .viewTimeout(10000) + .build(); + } + + @Override + public CouchbaseTemplate couchbaseTemplate() throws Exception { + CouchbaseTemplate template = super.couchbaseTemplate(); + template.setWriteResultChecking(WriteResultChecking.LOG); + return template; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/core/Beer.java b/src/test/java/org/springframework/data/couchbase/core/Beer.java new file mode 100644 index 00000000..3259ba3c --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/Beer.java @@ -0,0 +1,69 @@ +/* + * 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; + +import org.springframework.data.annotation.Id; +import org.springframework.data.couchbase.core.mapping.Field; + + +/** + * Test class for persisting and loading from {@link CouchbaseTemplate}. + * + * @author Michael Nitschinger + */ +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/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.java new file mode 100644 index 00000000..dad6ed97 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationServiceTests.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.translation; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; + +/** + * Verifies the functionality of a {@link JacksonTranslationService}. + * + * @author Michael Nitschinger + */ +public class JacksonTranslationServiceTests { + + private TranslationService service; + + @Before + public void setup() { + service = new JacksonTranslationService(); + } + + @Test + public void shouldEncodeNonASCII() { + CouchbaseDocument doc = new CouchbaseDocument("key"); + doc.put("language", "русский"); + String expected = "{\"language\":\"русский\"}"; + assertEquals(expected, service.encode(doc)); + } + + @Test + public void shouldDecodeNonASCII() { + String source = "{\"language\":\"русский\"}"; + CouchbaseDocument target = new CouchbaseDocument(); + service.decode(source, target); + assertEquals("русский", target.get("language")); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java new file mode 100644 index 00000000..0ae68c2c --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java @@ -0,0 +1,99 @@ +/* + * 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 static org.junit.Assert.assertEquals; + +import java.lang.reflect.Field; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; +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. + * + * @author Michael Nitschinger + */ +public class BasicCouchbasePersistentPropertyTests { + + /** + * 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(), PropertyNameFieldNamingStrategy.INSTANCE); + } + + /** + * Simple POJO to test attribute properties and annotations. + */ + public class Beer { + + @Id + private String id; + + @org.springframework.data.couchbase.core.mapping.Field("foobar") + String name; + + String description; + + public String getId() { + return id; + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java new file mode 100644 index 00000000..1fb609fb --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/CustomConvertersTests.java @@ -0,0 +1,186 @@ +/* + * 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 static org.junit.Assert.assertEquals; + +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.couchbase.TestApplicationConfig; +import org.springframework.data.couchbase.core.convert.CustomConversions; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Tests to verify custom mapping logic. + * + * @author Michael Nitschinger + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestApplicationConfig.class) +public class CustomConvertersTests { + + @Autowired + private MappingCouchbaseConverter converter; + + @After + public void cleanup() { + converter.setCustomConversions(new CustomConversions(Collections.emptyList())); + } + + @Test + public void shouldWriteWithCustomConverter() { + List converters = new ArrayList(); + converters.add(DateToStringConverter.INSTANCE); + converter.setCustomConversions(new CustomConversions(converters)); + converter.afterPropertiesSet(); + + Date date = new Date(); + BlogPost post = new BlogPost(); + post.created = date; + + CouchbaseDocument doc = new CouchbaseDocument(); + converter.write(post, doc); + + assertEquals(date.toString(), doc.getPayload().get("created")); + } + + @Test + public void shouldReadWithCustomConverter() { + List converters = new ArrayList(); + converters.add(IntegerToStringConverter.INSTANCE); + converter.setCustomConversions(new CustomConversions(converters)); + converter.afterPropertiesSet(); + + CouchbaseDocument doc = new CouchbaseDocument(); + doc.getPayload().put("content", 10); + Counter loaded = converter.read(Counter.class, doc); + assertEquals("even", loaded.content); + } + + @Test + public void shouldWriteConvertFullDocument() { + List converters = new ArrayList(); + converters.add(BlogPostToCouchbaseDocumentConverter.INSTANCE); + converter.setCustomConversions(new CustomConversions(converters)); + converter.afterPropertiesSet(); + + BlogPost post = new BlogPost(); + post.id = "foobar"; + post.title = "The Foo of the Bar"; + + CouchbaseDocument doc = new CouchbaseDocument(); + converter.write(post, doc); + + assertEquals("The Foo of the Bar", doc.getPayload().get("title")); + assertEquals("the_foo_of_the_bar", doc.getPayload().get("slug")); + } + + @Test + public void shouldReadConvertFullDocument() { + List converters = new ArrayList(); + converters.add(CouchbaseDocumentToBlogPostConverter.INSTANCE); + converter.setCustomConversions(new CustomConversions(converters)); + converter.afterPropertiesSet(); + + CouchbaseDocument doc = new CouchbaseDocument(); + doc.getPayload().put("title", "My Title"); + + BlogPost loaded = converter.read(BlogPost.class, doc); + assertEquals("modified", loaded.id); + assertEquals("My Title!!", loaded.title); + } + + public static class BlogPost { + @Id + public String id = "key"; + + @Field + public Date created; + + @Field + public String title; + + } + + public class Counter { + @Field + public String content; + } + + public static enum IntegerToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(Integer source) { + return source % 2 == 0 ? "even" : "odd"; + } + } + + public static enum DateToStringConverter implements Converter { + INSTANCE; + + public static Format FORMATTER = new SimpleDateFormat("yyyy HH"); + + @Override + public String convert(Date source) { + return source.toString(); + } + } + + @WritingConverter + public static enum BlogPostToCouchbaseDocumentConverter implements Converter { + INSTANCE; + + @Override + public CouchbaseDocument convert(BlogPost source) { + return new CouchbaseDocument() + .setId(source.id) + .put("title", source.title) + .put("slug", source.title.toLowerCase().replaceAll(" ", "_")); + } + } + + @ReadingConverter + public static enum CouchbaseDocumentToBlogPostConverter implements Converter { + INSTANCE; + + @Override + public BlogPost convert(CouchbaseDocument source) { + BlogPost post = new BlogPost(); + post.id = "modified"; + post.title = source.getPayload().get("title") + "!!"; + return post; + } + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java new file mode 100644 index 00000000..ea9a08ee --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java @@ -0,0 +1,664 @@ +/* + * 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 static org.junit.Assert.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.joda.time.LocalDateTime; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.couchbase.TestApplicationConfig; +import org.springframework.data.couchbase.core.convert.CustomConversions; +import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Michael Nitschinger + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = TestApplicationConfig.class) +public class MappingCouchbaseConverterTests { + + @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() throws Exception { + CouchbaseDocument converted = new CouchbaseDocument(); + StringEntity entity = new StringEntity("foobar"); + + converter.write(entity, converted); + Map result = converted.export(); + assertEquals(entity.getClass().getName(), result.get("_class")); + assertEquals("foobar", result.get("attr0")); + 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); + + converter.write(entity, converted); + Map result = converted.export(); + assertEquals(entity.getClass().getName(), result.get("_class")); + assertEquals(42L, result.get("attr0")); + 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); + + converter.write(entity, converted); + Map result = converted.export(); + assertEquals(entity.getClass().getName(), result.get("_class")); + assertEquals(true, result.get("attr0")); + 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); + + converter.write(entity, converted); + Map result = converted.export(); + assertEquals(entity.getClass().getName(), result.get("_class")); + assertEquals("a", result.get("attr0")); + assertEquals(5, result.get("attr1")); + assertEquals(-0.3, result.get("attr2")); + assertEquals(true, result.get("attr3")); + } + + @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 readsID() { + CouchbaseDocument document = new CouchbaseDocument("001"); + + BasicCouchbasePersistentPropertyTests.Beer beer = converter.read(BasicCouchbasePersistentPropertyTests.Beer.class, + document); + + assertEquals("001", beer.getId()); + } + + @Test + public void writesUninitializedValues() { + CouchbaseDocument converted = new CouchbaseDocument(); + UninitializedEntity entity = new UninitializedEntity(); + + converter.write(entity, converted); + Map result = converted.export(); + assertEquals(entity.getClass().getName(), result.get("_class")); + assertEquals(0, result.get("attr1")); + } + + @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); + + converter.write(entity, converted); + Map result = converted.export(); + assertEquals(attr0, result.get("attr0")); + assertEquals(attr1, result.get("attr1")); + assertEquals(attr2, result.get("attr2")); + assertEquals(attr3, result.get("attr3")); + + 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); + + converter.write(entity, converted); + Map result = converted.export(); + assertEquals(attr0, result.get("attr0")); + assertEquals(attr1, result.get("attr1")); + assertEquals(attr2, result.get("attr2")); + + 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); + assertEquals(2, readConverted.attr0.size()); + assertEquals(0, readConverted.attr1.size()); + assertEquals(1, readConverted.attr2.size()); + assertEquals(2, readConverted.attr2.get(0).size()); + } + + @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); + + converter.write(entity, converted); + Map result = converted.export(); + assertEquals(attr0.size(), ((Collection) result.get("attr0")).size()); + assertEquals(attr1.size(), ((Collection) result.get("attr1")).size()); + assertEquals(attr2.size(), ((Collection) result.get("attr2")).size()); + + 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 result = converted.export(); + + assertEquals(entity.getClass().getName(), result.get("_class")); + assertEquals(new HashMap() {{ + put("emailAddr", email); + }}, result.get("email")); + + 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); + } + + @Test + public void writesAndReadsCustomConvertedClass() { + List converters = new ArrayList(); + converters.add(BigDecimalToStringConverter.INSTANCE); + converters.add(StringToBigDecimalConverter.INSTANCE); + converter.setCustomConversions(new CustomConversions(converters)); + converter.afterPropertiesSet(); + + CouchbaseDocument converted = new CouchbaseDocument(); + + final String valueStr = "12.345"; + final BigDecimal value = new BigDecimal(valueStr); + final String value2Str = "0.6789"; + final BigDecimal value2 = new BigDecimal(value2Str); + List listOfValues = new ArrayList(); + listOfValues.add(value); + listOfValues.add(value2); + Map mapOfValues = new HashMap(); + mapOfValues.put("val1", value); + mapOfValues.put("val2", value2); + + CustomEntity entity = new CustomEntity(value, listOfValues, mapOfValues); + converter.write(entity, converted); + + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", CustomEntity.class.getName()); + source.put("value", valueStr); + CouchbaseList listOfValuesDoc = new CouchbaseList(); + listOfValuesDoc.put(valueStr); + listOfValuesDoc.put(value2Str); + source.put("listOfValues", listOfValuesDoc); + CouchbaseDocument mapOfValuesDoc = new CouchbaseDocument(); + mapOfValuesDoc.put("val1", valueStr); + mapOfValuesDoc.put("val2", value2Str); + source.put("mapOfValues", mapOfValuesDoc); + assertEquals(source.export().toString(), converted.export().toString()); + + CustomEntity readConverted = converter.read(CustomEntity.class, source); + assertEquals(value, readConverted.value); + assertEquals(listOfValues.get(0), readConverted.listOfValues.get(0)); + assertEquals(listOfValues.get(1), readConverted.listOfValues.get(1)); + assertEquals(mapOfValues.get("val1"), readConverted.mapOfValues.get("val1")); + assertEquals(mapOfValues.get("val2"), readConverted.mapOfValues.get("val2")); + } + + @Test + public void writesAndReadsClassContainingCustomConvertedObjects() { + List converters = new ArrayList(); + converters.add(BigDecimalToStringConverter.INSTANCE); + converters.add(StringToBigDecimalConverter.INSTANCE); + converter.setCustomConversions(new CustomConversions(converters)); + converter.afterPropertiesSet(); + + CouchbaseDocument converted = new CouchbaseDocument(); + + final String weightStr = "12.34"; + final BigDecimal weight = new BigDecimal(weightStr); + final CustomObject addy = new CustomObject(weight); + List listOfObjects = new ArrayList(); + listOfObjects.add(addy); + Map mapOfObjects = new HashMap(); + mapOfObjects.put("obj0", addy); + mapOfObjects.put("obj1", addy); + + CustomObjectEntity entity = new CustomObjectEntity(addy, listOfObjects, mapOfObjects); + converter.write(entity, converted); + + CouchbaseDocument source = new CouchbaseDocument(); + source.put("_class", CustomObjectEntity.class.getName()); + CouchbaseDocument objectDoc = new CouchbaseDocument(); + objectDoc.put("weight", weightStr); + source.put("object", objectDoc); + CouchbaseList listOfObjectsDoc = new CouchbaseList(); + listOfObjectsDoc.put(objectDoc); + source.put("listOfObjects", listOfObjectsDoc); + CouchbaseDocument mapOfObjectsDoc = new CouchbaseDocument(); + mapOfObjectsDoc.put("obj0", objectDoc); + mapOfObjectsDoc.put("obj1", objectDoc); + source.put("mapOfObjects", mapOfObjectsDoc); + assertEquals(source.export().toString(), converted.export().toString()); + + CustomObjectEntity readConverted = converter.read(CustomObjectEntity.class, source); + assertEquals(addy.weight, readConverted.object.weight); + assertEquals(listOfObjects.get(0).weight, readConverted.listOfObjects.get(0).weight); + assertEquals(mapOfObjects.get("obj0").weight, readConverted.mapOfObjects.get("obj0").weight); + assertEquals(mapOfObjects.get("obj1").weight, readConverted.mapOfObjects.get("obj1").weight); + } + + @Test + public void writesAndReadsDates() { + Date created = new Date(); + Calendar modified = Calendar.getInstance(); + LocalDateTime deleted = LocalDateTime.now(); + DateEntity entity = new DateEntity(created, modified, deleted); + + CouchbaseDocument converted = new CouchbaseDocument(); + converter.write(entity, converted); + assertEquals(created.getTime(), converted.getPayload().get("created")); + assertEquals(modified.getTimeInMillis() / 1000, converted.getPayload().get("modified")); + assertEquals(deleted.toDate().getTime(), converted.getPayload().get("deleted")); + + DateEntity read = converter.read(DateEntity.class, converted); + assertEquals(created.getTime(), read.created.getTime()); + assertEquals(modified.getTimeInMillis() / 1000, read.modified.getTimeInMillis() / 1000); + assertEquals(deleted.toDate().getTime(), read.deleted.toDate().getTime()); + } + + 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; + } + } + + static class CustomEntity extends BaseEntity { + private BigDecimal value; + private List listOfValues; + private Map mapOfValues; + + public CustomEntity(BigDecimal value, List listOfValues, Map mapOfValues) { + this.value = value; + this.listOfValues = listOfValues; + this.mapOfValues = mapOfValues; + } + } + + static class CustomObjectEntity extends BaseEntity { + private CustomObject object; + private List listOfObjects; + private Map mapOfObjects; + + public CustomObjectEntity(CustomObject object, List listOfObjects, Map mapOfObjects) { + this.object = object; + this.listOfObjects = listOfObjects; + this.mapOfObjects = mapOfObjects; + } + } + + static class CustomObject { + private BigDecimal weight; + + public CustomObject(BigDecimal weight) { + this.weight = weight; + } + } + + @WritingConverter + public static enum BigDecimalToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(BigDecimal source) { + return source.toPlainString(); + } + } + + @ReadingConverter + public static enum StringToBigDecimalConverter implements Converter { + INSTANCE; + + @Override + public BigDecimal convert(String source) { + return new BigDecimal(source); + } + } + + static class DateEntity extends BaseEntity { + private Date created; + private Calendar modified; + private LocalDateTime deleted; + + public DateEntity(Date created, Calendar modified, LocalDateTime deleted) { + this.created = created; + this.modified = modified; + this.deleted = deleted; + } + } + +}