From 341c41250d50f10a31bc7c36743c666de4f00afb Mon Sep 17 00:00:00 2001 From: Andrey Rubtsov Date: Sun, 31 Jul 2016 02:20:32 +0300 Subject: [PATCH] DATACOUCH-239 - Allow configurable expiry in @Document. Fixes #120 --- src/main/asciidoc/entity.adoc | 3 +- .../BasicCouchbasePersistentEntity.java | 64 +++++----- .../core/mapping/CouchbaseMappingContext.java | 2 +- .../data/couchbase/core/mapping/Document.java | 14 +++ .../BasicCouchbasePersistentEntityTests.java | 109 +++++++++++++++++- 5 files changed, 158 insertions(+), 34 deletions(-) diff --git a/src/main/asciidoc/entity.adoc b/src/main/asciidoc/entity.adoc index 8b5d08f4..c2c42647 100644 --- a/src/main/asciidoc/entity.adoc +++ b/src/main/asciidoc/entity.adoc @@ -58,7 +58,8 @@ public class User { ---- ==== -Couchbase Server supports automatic expiration for documents. The library implements support for it through the `@Document` annotation. You can set a `expiry` value which translates to the number of seconds until the document gets removed automatically. If you want to make it expire in 10 seconds after mutation, set it like `@Document(expiry = 10)`. +Couchbase Server supports automatic expiration for documents. The library implements support for it through the `@Document` annotation. You can set a `expiry` value which translates to the number of seconds until the document gets removed automatically. If you want to make it expire in 10 seconds after mutation, set it like `@Document(expiry = 10)`. Alternatively, you can configure the expiry using Spring's property support and the `expiryExpression` parameter, to allow for dynamically changing the expiry value. For example: `@Document(expiryExpression = "${valid.document.expiry}")`. The property must be resolvable to an int value and the two approaches cannot be mixed. + If you want a different representation of the field name inside the document in contrast to the field name used in your entity, you can set a different name on the `@Field` annotation. For example if you want to keep your documents small you can set the firstname field to `@Field("fname")`. In the JSON document, you'll see `{"fname": ".."}` instead of `{"firstname": ".."}`. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java index a70e07be..18347275 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java @@ -17,14 +17,12 @@ package org.springframework.data.couchbase.core.mapping; import com.couchbase.client.java.repository.annotation.Id; -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.context.EnvironmentAware; +import org.springframework.core.env.Environment; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.util.TypeInformation; -import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import java.util.Calendar; import java.util.TimeZone; @@ -36,12 +34,9 @@ import java.util.concurrent.TimeUnit; * @author Michael Nitschinger */ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity - implements CouchbasePersistentEntity, ApplicationContextAware { + implements CouchbasePersistentEntity, EnvironmentAware { - /** - * Contains the evaluation context. - */ - private final StandardEvaluationContext context; + private Environment environment; /** * Create a new entity. @@ -50,20 +45,21 @@ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity typeInformation) { super(typeInformation); - context = new StandardEvaluationContext(); + validateExpirationConfiguration(); + } + + private void validateExpirationConfiguration() { + Document annotation = getType().getAnnotation(Document.class); + if (annotation != null && annotation.expiry() > 0 && StringUtils.hasLength(annotation.expiryExpression())) { + String msg = String.format("Incorrect expiry configuration on class %s using %s. " + + "You cannot use 'expiry' and 'expiryExpression' at the same time", getType().getName(), annotation); + throw new IllegalArgumentException(msg); + } } - /** - * Sets the application context. - * - * @param applicationContext the application context. - * @throws BeansException if setting the application context did go wrong. - */ @Override - public void setApplicationContext(final ApplicationContext applicationContext) throws BeansException { - context.addPropertyAccessor(new BeanFactoryAccessor()); - context.setBeanResolver(new BeanFactoryResolver(applicationContext)); - context.setRootObject(applicationContext); + public void setEnvironment(Environment environment) { + this.environment = environment; } // DATACOUCH-145: allows SDK's @Id annotation to be used @@ -100,12 +96,13 @@ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity TTL_IN_SECONDS_INCLUSIVE_END) { //we want it to be represented as a UNIX timestamp style, seconds since Epoch in UTC Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); @@ -122,11 +119,26 @@ public class BasicCouchbasePersistentEntity extends BasicPersistentEntity 0 && annotation.touchOnRead(); + return annotation == null ? false : annotation.touchOnRead() && getExpiry() > 0; } } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java index 36d677ee..e75bf14d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java @@ -75,7 +75,7 @@ public class CouchbaseMappingContext protected BasicCouchbasePersistentEntity createPersistentEntity(final TypeInformation typeInformation) { BasicCouchbasePersistentEntity entity = new BasicCouchbasePersistentEntity(typeInformation); if (context != null) { - entity.setApplicationContext(context); + entity.setEnvironment(context.getEnvironment()); } return entity; } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java index a3f07ee3..f8e46919 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java @@ -29,6 +29,7 @@ import org.springframework.data.annotation.Persistent; * Identifies a domain object to be persisted to Couchbase. * * @author Michael Nitschinger + * @author Andrey Rubtsov */ @Persistent @Inherited @@ -38,9 +39,22 @@ public @interface Document { /** * An optional expiry time for the document. Default is no expiry. + * Only one of two might might be set at the same time: either {@link #expiry()} or {@link #expiryExpression()} */ int expiry() default 0; + /** + * Same as {@link #expiry} but allows the actual value to be set using standard Spring property sources mechanism. + * Only one might be set at the same time: either {@link #expiry()} or {@link #expiryExpression()}.
+ * Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}. + *

+ * The value will be recalculated for every {@link org.springframework.data.couchbase.core.CouchbaseTemplate} save/insert/update call, + * thus allowing actual expiration to reflect changes on-the-fly as soon as property sources change. + *

+ * SpEL is NOT supported. + */ + String expiryExpression() default ""; + /** * An optional time unit for the document's {@link #expiry()}, if set. Default is {@link TimeUnit#SECONDS}. */ diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java index 2ee24941..aeeb0eed 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntityTests.java @@ -16,26 +16,44 @@ package org.springframework.data.couchbase.core.mapping; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import org.junit.Rule; import org.junit.Test; - +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.mock.env.MockPropertySource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * Verifies the correct behavior of annotation at the class level on persistable objects. * * @author Simon Baslé */ +@RunWith(SpringJUnit4ClassRunner.class) +@TestPropertySource(properties = { + "valid.document.expiry = 10", + "invalid.document.expiry = abc" +}) +@ContextConfiguration(classes = BasicCouchbasePersistentEntityTests.class) public class BasicCouchbasePersistentEntityTests { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Autowired + ConfigurableEnvironment environment; + @Test public void testNoExpiryByDefault() { CouchbasePersistentEntity entity = new BasicCouchbasePersistentEntity( @@ -119,15 +137,58 @@ public class BasicCouchbasePersistentEntityTests { public void doesNotUseIsUpdateExpiryForRead() throws Exception { assertFalse(getBasicCouchbasePersistentEntity(SimpleDocument.class).isTouchOnRead()); assertFalse(getBasicCouchbasePersistentEntity(SimpleDocumentWithExpiry.class).isTouchOnRead()); - } + } @Test public void usesTouchOnRead() throws Exception { assertTrue(getBasicCouchbasePersistentEntity(SimpleDocumentWithTouchOnRead.class).isTouchOnRead()); } + @Test + public void usesGetExpiryExpression() throws Exception { + assertEquals(10, getBasicCouchbasePersistentEntity(ConstantExpiryExpression.class).getExpiry()); + } + + @Test + public void usesGetExpiryFromValidExpression() throws Exception { + assertEquals(10, getBasicCouchbasePersistentEntity(ExpiryWithValidExpression.class).getExpiry()); + } + + @Test + public void doesNotAllowUseExpiryFromInvalidExpression() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Invalid Integer value for expiry expression: abc"); + assertEquals(10, getBasicCouchbasePersistentEntity(ExpiryWithInvalidExpression.class).getExpiry()); + } + + @Test + public void usesGetExpiryExpressionAndRespectsPropertyUpdates() throws Exception { + BasicCouchbasePersistentEntity entity = getBasicCouchbasePersistentEntity(ExpiryWithValidExpression.class); + assertEquals(10, entity.getExpiry()); + + environment.getPropertySources().addFirst(new MockPropertySource().withProperty("valid.document.expiry", "20")); + assertEquals(20, entity.getExpiry()); + } + + @Test + public void failsIfExpiryExpressionMissesRequiredProperty() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Could not resolve placeholder 'missing.expiry'"); + getBasicCouchbasePersistentEntity(ExpiryWithMissingProperty.class).getExpiry(); + } + + @Test + public void doesNotAllowUseExpiryAndExpressionSimultaneously() throws Exception { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("You cannot use 'expiry' and 'expiryExpression' at the same time"); + expectedException.expectMessage(ExpiryAndExpression.class.getName()); + getBasicCouchbasePersistentEntity(ExpiryAndExpression.class).getExpiry(); + } + private BasicCouchbasePersistentEntity getBasicCouchbasePersistentEntity(Class clazz) { - return new BasicCouchbasePersistentEntity(ClassTypeInformation.from(clazz)); + BasicCouchbasePersistentEntity basicCouchbasePersistentEntity = new BasicCouchbasePersistentEntity(ClassTypeInformation.from(clazz)); + basicCouchbasePersistentEntity.setEnvironment(environment); + return basicCouchbasePersistentEntity; } public static class SimpleDocument { @@ -175,4 +236,40 @@ public class BasicCouchbasePersistentEntityTests { @Document(expiry = 31 * 24 * 60 * 60) public class OverLimitSecondsExpiry { } + + /** + * Simple POJO to test constant expiry expression + */ + @Document(expiryExpression = "10") + private class ConstantExpiryExpression { + } + + /** + * Simple POJO to test valid expiry expression by resolving simple property from environment + */ + @Document(expiryExpression = "${valid.document.expiry}") + private class ExpiryWithValidExpression { + } + + /** + * Simple POJO to test invalid expiry expression + */ + @Document(expiryExpression = "${invalid.document.expiry}") + private class ExpiryWithInvalidExpression { + } + + /** + * Simple POJO to test expiry expression logic failure to resolve property placeholder + */ + @Document(expiryExpression = "${missing.expiry}") + private class ExpiryWithMissingProperty { + } + + /** + * Simple POJO to test that expiry and expiry expression cannot be used simultaneously + */ + @Document(expiry = 10, expiryExpression = "10") + private class ExpiryAndExpression { + } + }