committed by
Simon Baslé
parent
415d8a2d23
commit
341c41250d
@@ -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": ".."}`.
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user