diff --git a/README.md b/README.md index 1e2b498..fda9d53 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ To get the binaries of this library as distributed by Microsoft, ready for use w com.microsoft.azure azure-documentdb - 1.6.0 + 1.7.0 ###Option 2: Source Via Git @@ -92,9 +92,9 @@ public class SampleApp { DocumentCollection myCollection = new DocumentCollection(); myCollection.setId(COLLECTION_ID); - // Configure the new collection performance tier to S1. + // Set the provisioned throughput for this collection to be 1000 RUs. RequestOptions requestOptions = new RequestOptions(); - requestOptions.setOfferType("S1"); + requestOptions.setOfferThroughput(1000); // Create a new collection. myCollection = documentClient.createCollection( diff --git a/changelog.md b/changelog.md index a3896fd..788484c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,7 @@ +## Changes in 1.7.0 : ## + + - Added support for expiring documents by setting the default time-to-live on collections and time-to-live override on documents. + ## Changes in 1.6.0 : ## - Added support to set offer throughput for collections created with variable pricing structure. diff --git a/pom.xml b/pom.xml index fb54c01..e880b41 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.microsoft.azure azure-documentdb - 1.6.0 + 1.7.0 ${project.groupId}:${project.artifactId} Java SDK for Microsoft Azure DocumentDB http://azure.microsoft.com/en-us/services/documentdb/ diff --git a/src/com/microsoft/azure/documentdb/Constants.java b/src/com/microsoft/azure/documentdb/Constants.java index 5aae642..4e7a588 100644 --- a/src/com/microsoft/azure/documentdb/Constants.java +++ b/src/com/microsoft/azure/documentdb/Constants.java @@ -117,7 +117,11 @@ static class Properties { static final String PARTITION_KEY_PATHS = "paths"; static final String PARTITION_KIND = "kind"; static final String RESOURCE_PARTITION_KEY = "resourcePartitionKey"; - } + + //Time-to-Live + static final String TTL = "ttl"; + static final String DEFAULT_TTL = "defaultTtl"; +} static class ResourceKeys { static final String ATTACHMENTS = "Attachments"; diff --git a/src/com/microsoft/azure/documentdb/Document.java b/src/com/microsoft/azure/documentdb/Document.java index 17f4c3b..0c4adb7 100644 --- a/src/com/microsoft/azure/documentdb/Document.java +++ b/src/com/microsoft/azure/documentdb/Document.java @@ -53,4 +53,41 @@ static Document FromObject(Object document) { } return typedDocument; } + + /** + * Sets the document's time-to-live value. + *

+ * A document's time-to-live value is an optional property. If set, the document expires after the specified number + * of seconds since its last write time. The value of this property should be one of the following: + * null - indicates the time-to-live value for this document inherits from the parent collection's default time-to-live value. + * nonzero positive integer - indicates the number of seconds before the document expires. It overrides the default time-to-live + * value specified on the parent collection, unless the parent collection's default time-to-live is null. + * -1 - indicates the document never expires. It overrides the default time-to-live + * value specified on the parent collection, unless the parent collection's default time-to-live is null. + * + * @param timeToLive the document's time-to-live value in seconds. + */ + public void setTimeToLive(Integer timeToLive) { + // a "null" value is represented as a missing element on the wire. + // setting timeToLive to null should remove the property from the property bag. + if (timeToLive != null) { + super.set(Constants.Properties.TTL, timeToLive); + } + else if (super.has(Constants.Properties.TTL)) { + super.remove(Constants.Properties.TTL); + } + } + + /** + * Gets the document's time-to-live value. + * + * @return the document's time-to-live value in seconds. + */ + public Integer getTimeToLive() { + if (super.has(Constants.Properties.TTL)) { + return super.getInt(Constants.Properties.TTL); + } + + return null; + } } diff --git a/src/com/microsoft/azure/documentdb/DocumentCollection.java b/src/com/microsoft/azure/documentdb/DocumentCollection.java index 563a554..9c73946 100644 --- a/src/com/microsoft/azure/documentdb/DocumentCollection.java +++ b/src/com/microsoft/azure/documentdb/DocumentCollection.java @@ -95,6 +95,44 @@ public PartitionKeyDefinition getPartitionKey() { return null; } + /** + * Sets the collection's default time-to-live value. + *

+ * The default time-to-live value on a collection is an optional property. If set, the documents within the collection + * expires after the specified number of seconds since their last write time. The value of this property should be one of the following: + * null - indicates evaluation of time-to-live is disabled and documents within the collection will never expire, regardless whether + * individual documents have their time-to-live set. + * nonzero positive integer - indicates the default time-to-live value for all documents within the collection. This value can be overridden + * by individual documents' time-to-live value. + * -1 - indicates by default all documents within the collection never expire. This value can be overridden by individual documents' + * time-to-live value. + * + * @param timeToLive the default time-to-live value in seconds. + */ + public void setDefaultTimeToLive(Integer timeToLive) { + // a "null" value is represented as a missing element on the wire. + // setting timeToLive to null should remove the property from the property bag. + if (timeToLive != null) { + super.set(Constants.Properties.DEFAULT_TTL, timeToLive); + } + else if (super.has(Constants.Properties.DEFAULT_TTL)) { + super.remove(Constants.Properties.DEFAULT_TTL); + } + } + + /** + * Gets the collection's default time-to-live value. + * + * @return the the default time-to-live value in seconds. + */ + public Integer getDefaultTimeToLive() { + if (super.has(Constants.Properties.DEFAULT_TTL)) { + return super.getInt(Constants.Properties.DEFAULT_TTL); + } + + return null; + } + /** * Gets the self-link for documents in a collection. * diff --git a/src/com/microsoft/azure/documentdb/HashPartitionResolver.java b/src/com/microsoft/azure/documentdb/HashPartitionResolver.java index bde2f88..da938b7 100644 --- a/src/com/microsoft/azure/documentdb/HashPartitionResolver.java +++ b/src/com/microsoft/azure/documentdb/HashPartitionResolver.java @@ -130,7 +130,7 @@ public String resolveForCreate(Object document) { /** * Resolves the collection for reading/querying the documents based on the partition key. * - * @param partitionKey the partition key used to resolve the collection. + * @param partitionKey the partition key value * * @return collection Self link(s) or Name based link(s) which should handle the Read operation */ diff --git a/src/com/microsoft/azure/documentdb/HttpConstants.java b/src/com/microsoft/azure/documentdb/HttpConstants.java index 0c30999..74a3155 100644 --- a/src/com/microsoft/azure/documentdb/HttpConstants.java +++ b/src/com/microsoft/azure/documentdb/HttpConstants.java @@ -166,7 +166,7 @@ public static class HttpHeaders { public static class Versions { public static String CURRENT_VERSION = "2015-12-16"; - public static String USER_AGENT = "documentdb-java-sdk-1.6.0"; + public static String USER_AGENT = "documentdb-java-sdk-1.7.0"; } public static class StatusCodes { diff --git a/src/com/microsoft/azure/documentdb/PartitionKey.java b/src/com/microsoft/azure/documentdb/PartitionKey.java index 9219933..b268a46 100644 --- a/src/com/microsoft/azure/documentdb/PartitionKey.java +++ b/src/com/microsoft/azure/documentdb/PartitionKey.java @@ -28,7 +28,7 @@ public PartitionKey(Object key) { * Create a new instance of the PartitionKey object from a serialized JSON string. * * @param jsonString - * the value of the partition key in JSON form. + * the JSON string representation of this PartitionKey object. * * @return the PartitionKey instance. */ diff --git a/src/com/microsoft/azure/documentdb/RangePartitionResolver.java b/src/com/microsoft/azure/documentdb/RangePartitionResolver.java index ec88343..f49b22f 100644 --- a/src/com/microsoft/azure/documentdb/RangePartitionResolver.java +++ b/src/com/microsoft/azure/documentdb/RangePartitionResolver.java @@ -81,7 +81,7 @@ public String resolveForCreate(Object document) { /** * Resolves the collection for reading/querying the documents based on the partition key. * - * @param partitionKey the partition key used to resolve the collection. + * @param partitionKey the partition key value. * * @return collection Self link(s) or Name based link(s) which should handle the Read operation. */ diff --git a/src/com/microsoft/azure/documentdb/test/GatewayTestBase.java b/src/com/microsoft/azure/documentdb/test/GatewayTestBase.java index 9f6ab2f..b6b8a2a 100644 --- a/src/com/microsoft/azure/documentdb/test/GatewayTestBase.java +++ b/src/com/microsoft/azure/documentdb/test/GatewayTestBase.java @@ -46,7 +46,7 @@ public class GatewayTestBase { // Replace MASTER_KEY and HOST with values from your DocumentDB account. protected static final String MASTER_KEY = "[REPLACE WITH YOUR APP MASTER KEY]"; protected static final String HOST = "[REPLACE WITH YOUR APP ENDPOINT, FOR EXAMPLE https://myapp.documents.azure.com:443]"; - + protected static final String DATABASES_PATH_SEGMENT = "dbs"; protected static final String USERS_PATH_SEGMENT = "users"; protected static final String PERMISSIONS_PATH_SEGMENT = "permissions"; diff --git a/src/com/microsoft/azure/documentdb/test/JavaMultiPartitionCollectionTests.java b/src/com/microsoft/azure/documentdb/test/JavaMultiPartitionCollectionTests.java index 4ece2b9..947c068 100644 --- a/src/com/microsoft/azure/documentdb/test/JavaMultiPartitionCollectionTests.java +++ b/src/com/microsoft/azure/documentdb/test/JavaMultiPartitionCollectionTests.java @@ -137,7 +137,7 @@ public void testDocumentCrud() throws DocumentClientException { Assert.assertTrue(readFail); // Read document1 with partition key. - document = client.readDocument(document.getSelfLink(), requestOptions).getResource(); + document = this.client.readDocument(document.getSelfLink(), requestOptions).getResource(); Assert.assertEquals(documentId, document.getString("id")); // Update document without partition key. @@ -162,11 +162,11 @@ public void testDocumentCrud() throws DocumentClientException { Assert.assertTrue(deleteFail); // Delete document with partition key. - client.deleteDocument(document.getSelfLink(), requestOptions); + this.client.deleteDocument(document.getSelfLink(), requestOptions); readFail = false; try { - client.readDocument(document.getSelfLink(), requestOptions).getResource(); + this.client.readDocument(document.getSelfLink(), requestOptions).getResource(); } catch (DocumentClientException e) { readFail = true; Assert.assertEquals(404, e.getStatusCode()); @@ -190,7 +190,7 @@ public void testDocumentKeyExtraction() throws DocumentClientException { // create document without partition key String documentId = GatewayTests.getUID(); Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId, documentId)); - Document document = client.createDocument( + Document document = this.client.createDocument( createdCollection.getSelfLink(), sampleDocument, null, @@ -220,7 +220,7 @@ public void testDocumentKeyExtractionNestedProperty() throws DocumentClientExcep String documentId = GatewayTests.getUID(); Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId, documentId)); - Document document = client.createDocument( + Document document = this.client.createDocument( createdCollection.getSelfLink(), sampleDocument, null, @@ -249,7 +249,7 @@ public void testDocumentKeyExtractionUndefined() throws DocumentClientException String documentId = GatewayTests.getUID(); Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId)); - Document document = client.createDocument( + Document document = this.client.createDocument( createdCollection.getSelfLink(), sampleDocument, null, @@ -279,7 +279,7 @@ public void testDocumentKeyExtractionComplextTypeAsUndefined() throws DocumentCl String documentId = GatewayTests.getUID(); Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId)); - Document document = client.createDocument( + Document document = this.client.createDocument( createdCollection.getSelfLink(), sampleDocument, null, @@ -308,7 +308,7 @@ public void testDocumentKeyExtractionWithEscapeCharacters1() throws DocumentClie String documentId = GatewayTests.getUID(); Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId, documentId)); - Document document = client.createDocument( + Document document = this.client.createDocument( createdCollection.getSelfLink(), sampleDocument, null, @@ -337,7 +337,7 @@ public void testDocumentKeyExtractionWithEscapeCharacters2() throws DocumentClie String documentId = GatewayTests.getUID(); Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId, documentId)); - Document document = client.createDocument( + Document document = this.client.createDocument( createdCollection.getSelfLink(), sampleDocument, null, @@ -368,7 +368,7 @@ public void testNullPartitionKey() throws DocumentClientException { String documentId = GatewayTests.getUID(); String name = JSONObject.NULL.toString(); Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId, name)); - Document document = client.createDocument( + Document document = this.client.createDocument( createdCollection.getSelfLink(), sampleDocument, null, @@ -416,20 +416,20 @@ public void testReadDocumentFeed() throws DocumentClientException { FeedOptions feedOptions = new FeedOptions(); feedOptions.setEnableCrossPartitionQuery(true); - List documents = client.readDocuments(createdCollection.getSelfLink(), feedOptions).getQueryIterable().toList(); + List documents = this.client.readDocuments(createdCollection.getSelfLink(), feedOptions).getQueryIterable().toList(); Assert.assertEquals(2, documents.size()); // Read document feed with partition key. feedOptions = new FeedOptions(); feedOptions.setEnableCrossPartitionQuery(true); feedOptions.setPartitionKey(new PartitionKey(documentId1)); - documents = client.readDocuments(createdCollection.getSelfLink(), feedOptions).getQueryIterable().toList(); + documents = this.client.readDocuments(createdCollection.getSelfLink(), feedOptions).getQueryIterable().toList(); Assert.assertEquals(1, documents.size()); Assert.assertEquals(sampleDocument1.getString("id"), documents.get(0).getString("id")); feedOptions = new FeedOptions(); feedOptions.setPartitionKey(new PartitionKey(documentId2)); - documents = client.readDocuments(createdCollection.getSelfLink(), feedOptions).getQueryIterable().toList(); + documents = this.client.readDocuments(createdCollection.getSelfLink(), feedOptions).getQueryIterable().toList(); Assert.assertEquals(1, documents.size()); Assert.assertEquals(sampleDocument2.getString("id"), documents.get(0).getString("id")); } @@ -625,7 +625,7 @@ public void testExecuteStoredProcedure() throws DocumentClientException { @Test public void testCreatePermissionWithPartitionKey() throws DocumentClientException { // Create user. - User user = client.createUser( + User user = this.client.createUser( getDatabaseLink(this.databaseForTest, true), new User("{ 'id': 'new user' }"), null).getResource(); diff --git a/src/com/microsoft/azure/documentdb/test/JavaTtlTests.java b/src/com/microsoft/azure/documentdb/test/JavaTtlTests.java new file mode 100644 index 0000000..938e1f2 --- /dev/null +++ b/src/com/microsoft/azure/documentdb/test/JavaTtlTests.java @@ -0,0 +1,254 @@ +package com.microsoft.azure.documentdb.test; + +import org.junit.Assert; +import org.junit.Test; + +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.RequestOptions; + +public class JavaTtlTests extends GatewayTestBase { + + @Test + public void testCollectionDefaultTtl() throws DocumentClientException { + String collectionId = GatewayTestBase.getUID(); + RequestOptions options = new RequestOptions(); + options.setOfferThroughput(400); + + Integer ttl = 5; + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setDefaultTimeToLive(ttl); + + collectionDefinition.setId(collectionId); + DocumentCollection createdCollection = this.client.createCollection( + getDatabaseLink(this.databaseForTest, true), + collectionDefinition, + options).getResource(); + Assert.assertEquals(ttl, createdCollection.getDefaultTimeToLive()); + + String sampleDocumentTemplate = + "{" + + " 'id': '%s'," + + " 'name': 'sample document %s'," + + "}"; + String documentId = GatewayTests.getUID(); + Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId, documentId)); + Document createdDocument = this.client.createDocument( + createdCollection.getSelfLink(), + sampleDocument, + null, + false).getResource(); + + Document document = this.client.readDocument(createdDocument.getSelfLink(), null).getResource(); + Assert.assertEquals(documentId, document.getString("id")); + + // Wait for document to expired and be deleted in the background. + try { + Thread.sleep(25000); + } catch (InterruptedException e) { + Assert.assertTrue("Sleep interrupted.", false); + } + + Boolean readFail = false; + try { + document = this.client.readDocument(createdDocument.getSelfLink(), null).getResource(); + } catch (DocumentClientException exp) { + Assert.assertEquals(404, exp.getStatusCode()); + Assert.assertEquals("NotFound", exp.getError().getCode()); + readFail = true; + } + Assert.assertTrue(readFail); + } + + @Test + public void testDocumentOverrideCollectionTtlPositive() throws DocumentClientException { + this.testDocumentOverride(60, 5, 6, false); + this.testDocumentOverride(5, 60, 6, true); + this.testDocumentOverride(5, -1, 6, true); + this.testDocumentOverride(5, null, 6, false); + } + + @Test + public void testDocumentOverrideCollectionTtlNegative() throws DocumentClientException { + this.testDocumentOverride(-1, 5, 6, false); + this.testDocumentOverride(-1, null, 6, true); + } + + @Test + public void testDocumentOverrideCollectionTtlNull() throws DocumentClientException { + this.testDocumentOverride(null, 2, 5, true); + this.testDocumentOverride(null, -1, 5, true); + } + + @Test + public void testRemoveDefaultTtl() throws DocumentClientException { + String collectionId = GatewayTestBase.getUID(); + RequestOptions options = new RequestOptions(); + options.setOfferThroughput(400); + + Integer defaultTtl = 5; + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setDefaultTimeToLive(defaultTtl); + + collectionDefinition.setId(collectionId); + DocumentCollection createdCollection = this.client.createCollection( + getDatabaseLink(this.databaseForTest, true), + collectionDefinition, + options).getResource(); + Assert.assertEquals(collectionId, createdCollection.getString("id")); + Assert.assertEquals(defaultTtl, createdCollection.getDefaultTimeToLive()); + + // Remove the default ttl by setting it to null. + createdCollection.setDefaultTimeToLive(null); + createdCollection = this.client.replaceCollection(createdCollection, null).getResource(); + Assert.assertNull(createdCollection.getDefaultTimeToLive()); + + String sampleDocumentTemplate = + "{" + + " 'id': '%s'," + + " 'name': 'sample document %s'," + + "}"; + String documentId = GatewayTests.getUID(); + Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId, documentId)); + Document createdDocument = this.client.createDocument( + createdCollection.getSelfLink(), + sampleDocument, + null, + false).getResource(); + + try { + Thread.sleep(6000); + } catch (InterruptedException e) { + Assert.assertTrue("Sleep interrupted.", false); + } + + // Trigger TTL evaluation by adding another document to the collection. + String documentId2 = GatewayTests.getUID(); + Document sampleDocument2 = new Document(String.format(sampleDocumentTemplate, documentId2, documentId2)); + this.client.createDocument( + createdCollection.getSelfLink(), + sampleDocument2, + null, + false).getResource(); + + createdDocument = this.client.readDocument(createdDocument.getSelfLink(), null).getResource(); + Assert.assertNotNull(createdDocument); + } + + @Test + public void testRemoveDocumentTtl() throws DocumentClientException { + String collectionId = GatewayTestBase.getUID(); + RequestOptions options = new RequestOptions(); + options.setOfferThroughput(400); + + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setDefaultTimeToLive(-1); + collectionDefinition.setId(collectionId); + DocumentCollection createdCollection = this.client.createCollection( + getDatabaseLink(this.databaseForTest, true), + collectionDefinition, + options).getResource(); + + String sampleDocumentTemplate = + "{" + + " 'id': '%s'," + + " 'name': 'sample document %s'," + + "}"; + String documentId = GatewayTests.getUID(); + Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId, documentId)); + Integer ttl = 5; + sampleDocument.setTimeToLive(ttl); + Document createdDocument = this.client.createDocument( + createdCollection.getSelfLink(), + sampleDocument, + null, + false).getResource(); + Assert.assertEquals(ttl, createdDocument.getTimeToLive()); + + // Remove document ttl by setting it to null. + createdDocument.setTimeToLive(null); + createdDocument = this.client.replaceDocument(createdDocument, null).getResource(); + Assert.assertNull(createdDocument.getTimeToLive()); + + try { + Thread.sleep(6000); + } catch (InterruptedException e) { + Assert.assertTrue("Sleep interrupted.", false); + } + + // Trigger TTL evaluation by adding another document to the collection. + String documentId2 = GatewayTests.getUID(); + Document sampleDocument2 = new Document(String.format(sampleDocumentTemplate, documentId2, documentId2)); + this.client.createDocument( + createdCollection.getSelfLink(), + sampleDocument2, + null, + false).getResource(); + + createdDocument = this.client.readDocument(createdDocument.getSelfLink(), null).getResource(); + Assert.assertNotNull(createdDocument); + } + + private void testDocumentOverride( + Integer collectionDefaultTtl, + Integer documentTtl, + Integer sleepSeconds, + Boolean documentExists) throws DocumentClientException { + String collectionId = GatewayTestBase.getUID(); + RequestOptions options = new RequestOptions(); + options.setOfferThroughput(400); + + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setDefaultTimeToLive(collectionDefaultTtl); + + collectionDefinition.setId(collectionId); + DocumentCollection createdCollection = this.client.createCollection( + getDatabaseLink(this.databaseForTest, true), + collectionDefinition, + options).getResource(); + + String sampleDocumentTemplate = + "{" + + " 'id': '%s'," + + " 'name': 'sample document %s'," + + "}"; + String documentId = GatewayTests.getUID(); + Document sampleDocument = new Document(String.format(sampleDocumentTemplate, documentId, documentId)); + sampleDocument.setTimeToLive(documentTtl); + Document createdDocument = this.client.createDocument( + createdCollection.getSelfLink(), + sampleDocument, + null, + false).getResource(); + + Document document = this.client.readDocument(createdDocument.getSelfLink(), null).getResource(); + Assert.assertEquals(documentId, document.getString("id")); + + try { + Thread.sleep(sleepSeconds * 1000); + } catch (InterruptedException e) { + Assert.assertTrue("Sleep interrupted.", false); + } + + // Trigger TTL evaluation by adding another document to the collection. + String anotherDocumentId = GatewayTests.getUID(); + Document anotherDocument = new Document(String.format(sampleDocumentTemplate, anotherDocumentId, anotherDocumentId)); + anotherDocument = this.client.createDocument( + createdCollection.getSelfLink(), + anotherDocument, + null, + false).getResource(); + + Boolean documentFound = false; + try { + document = this.client.readDocument(createdDocument.getSelfLink(), null).getResource(); + Assert.assertNotNull(document); + documentFound = true; + } catch (DocumentClientException exp) { + Assert.assertEquals(404, exp.getStatusCode()); + Assert.assertEquals("NotFound", exp.getError().getCode()); + } + Assert.assertEquals(documentExists, documentFound); + } +} \ No newline at end of file