DATACOUCH-239 - Allow configurable expiry in @Document.

Fixes #120
This commit is contained in:
Andrey Rubtsov
2016-07-31 02:20:32 +03:00
committed by Simon Baslé
parent 415d8a2d23
commit 341c41250d
5 changed files with 158 additions and 34 deletions

View File

@@ -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": ".."}`.

View File

@@ -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<T> extends BasicPersistentEntity<T, CouchbasePersistentProperty>
implements CouchbasePersistentEntity<T>, ApplicationContextAware {
implements CouchbasePersistentEntity<T>, EnvironmentAware {
/**
* Contains the evaluation context.
*/
private final StandardEvaluationContext context;
private Environment environment;
/**
* Create a new entity.
@@ -50,20 +45,21 @@ public class BasicCouchbasePersistentEntity<T> extends BasicPersistentEntity<T,
*/
public BasicCouchbasePersistentEntity(final TypeInformation<T> 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<T> extends BasicPersistentEntity<T,
@Override
public int getExpiry() {
org.springframework.data.couchbase.core.mapping.Document annotation =
getType().getAnnotation(org.springframework.data.couchbase.core.mapping.Document.class);
Document annotation = getType().getAnnotation(Document.class);
if (annotation == null)
return 0;
long secondsShift = annotation.expiryUnit().toSeconds(annotation.expiry());
int expiryValue = getExpiryValue(annotation);
long secondsShift = annotation.expiryUnit().toSeconds(expiryValue);
if (secondsShift > 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<T> extends BasicPersistentEntity<T,
}
}
private int getExpiryValue(Document annotation) {
int expiryValue = annotation.expiry();
String expiryExpressionString = annotation.expiryExpression();
if (StringUtils.hasLength(expiryExpressionString)) {
Assert.notNull(environment, "Environment must be set to use 'expiryExpression'");
String expiryWithReplacedPlaceholders = environment.resolveRequiredPlaceholders(expiryExpressionString);
try {
expiryValue = Integer.parseInt(expiryWithReplacedPlaceholders);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid Integer value for expiry expression: " + expiryWithReplacedPlaceholders);
}
}
return expiryValue;
}
@Override
public boolean isTouchOnRead() {
org.springframework.data.couchbase.core.mapping.Document annotation = getType().getAnnotation(
org.springframework.data.couchbase.core.mapping.Document.class);
return annotation == null ? false : getExpiry() > 0 && annotation.touchOnRead();
return annotation == null ? false : annotation.touchOnRead() && getExpiry() > 0;
}
}

View File

@@ -75,7 +75,7 @@ public class CouchbaseMappingContext
protected <T> BasicCouchbasePersistentEntity<?> createPersistentEntity(final TypeInformation<T> typeInformation) {
BasicCouchbasePersistentEntity<T> entity = new BasicCouchbasePersistentEntity<T>(typeInformation);
if (context != null) {
entity.setApplicationContext(context);
entity.setEnvironment(context.getEnvironment());
}
return entity;
}

View File

@@ -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()}. <br />
* Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}.
* <br /><br />
* 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.
* <br /><br />
* SpEL is NOT supported.
*/
String expiryExpression() default "";
/**
* An optional time unit for the document's {@link #expiry()}, if set. Default is {@link TimeUnit#SECONDS}.
*/

View File

@@ -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<DefaultExpiry> entity = new BasicCouchbasePersistentEntity<DefaultExpiry>(
@@ -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 {
}
}