diff --git a/azure-documentdb-examples/pom.xml b/azure-documentdb-examples/pom.xml new file mode 100644 index 000000000000..c4a3891f91ee --- /dev/null +++ b/azure-documentdb-examples/pom.xml @@ -0,0 +1,97 @@ + + 4.0.0 + + com.microsoft.azure + azure-documentdb-examples + 0.0.1-SNAPSHOT + jar + + azure-documentdb-examples + http://azure.microsoft.com/en-us/services/documentdb/ + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + + UTF-8 + 1.7.6 + 1.2.17 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.8 + + + + org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8 + + + + + + + + + com.microsoft.azure + azure-documentdb-rx + 0.9.0-SNAPSHOT + + + io.reactivex + rxjava-guava + 1.0.3 + + + junit + junit + 4.12 + test + + + org.mockito + mockito-core + 1.10.19 + test + + + org.hamcrest + hamcrest-all + 1.3 + test + + + org.slf4j + slf4j-api + ${slf4j.version} + test + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + test + + + log4j + log4j + ${log4j.version} + test + + + diff --git a/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/DatabaseAndCollectionCreationAsyncAPITest.java b/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/DatabaseAndCollectionCreationAsyncAPITest.java new file mode 100644 index 000000000000..1ea1cf0a7140 --- /dev/null +++ b/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/DatabaseAndCollectionCreationAsyncAPITest.java @@ -0,0 +1,256 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.examples; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.util.concurrent.ListenableFuture; +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.SqlParameter; +import com.microsoft.azure.documentdb.SqlParameterCollection; +import com.microsoft.azure.documentdb.SqlQuerySpec; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; +import com.microsoft.azure.documentdb.rx.examples.TestConfigurations; + +import rx.Observable; +import rx.functions.Action1; +import rx.observable.ListenableFutureObservable; + +/** + * This integration test class demonstrates how to use Async API to create, + * delete, replace, and update. + * + * NOTE: you can use rxJava based async api with java8 lambda expression. Using of + * rxJava based async APIs with java8 lambda expressions is much prettier. + * + * You can also use the async API without java8 lambda expression support. + * + * For example + * + * + * Also if you need to work with Future or ListenableFuture it is possible to transform + * an observable to ListenableFuture. Please see {@link #testTransformObservableToGoogleGuavaListenableFuture()} + * + */ +public class DatabaseAndCollectionCreationAsyncAPITest { + + private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseAndCollectionCreationAsyncAPITest.class); + + private static final String DATABASE_ID = "async-test-db"; + private DocumentCollection collectionDefinition; + private Database databaseDefinition; + + private AsyncDocumentClient asyncClient; + + @Before + public void setUp() throws DocumentClientException { + + asyncClient = new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(ConnectionPolicy.GetDefault()) + .withConsistencyLevel(ConsistencyLevel.Session) + .build(); + + // Clean up before setting up + this.cleanUpGeneratedDatabases(); + + databaseDefinition = new Database(); + databaseDefinition.setId(DATABASE_ID); + + collectionDefinition = new DocumentCollection(); + collectionDefinition.setId(UUID.randomUUID().toString()); + } + + @After + public void shutdown() throws DocumentClientException { + asyncClient.close(); + } + + @Test + public void testCreateDatabase_Async() throws Exception { + + // create a database using async api + // this test uses java8 lambda expression see testCreateDatabase_Async_withoutLambda + + Observable> createDatabaseObservable = asyncClient + .createDatabase(databaseDefinition, null); + + final CountDownLatch doneLatch = new CountDownLatch(1); + + createDatabaseObservable + .single() // we know there is only single result + .subscribe( + databaseResourceResponse -> { + System.out.println(databaseResourceResponse.getActivityId()); + doneLatch.countDown(); + }, + + error -> { + System.err.println("an error happened in database creation: actual cause: " + error.getMessage()); + } + ); + + // wait till database creation completes + doneLatch.await(); + } + + @Test + public void testCreateDatabase_Async_withoutLambda() throws Exception { + + // create a database using async api + + Observable> createDatabaseObservable = asyncClient + .createDatabase(databaseDefinition, null); + + final CountDownLatch successfulCompletionLatch = new CountDownLatch(1); + Action1> onDatabaseCreationAction = new Action1>() { + + @Override + public void call(ResourceResponse resourceResponse) { + // Database is created + System.out.println(resourceResponse.getActivityId()); + successfulCompletionLatch.countDown(); + } + }; + + Action1 onError = new Action1() { + @Override + public void call(Throwable error) { + System.err.println("an error happened in database creation: actual cause: " + error.getMessage()); + } + }; + + createDatabaseObservable + .single() //we know there is only a single event + .subscribe(onDatabaseCreationAction, onError); + + // wait till database creation completes + successfulCompletionLatch.await(); + } + + + @Test + public void testCreateDatabase_toBlocking() throws DocumentClientException { + + // create a database + // toBlocking() converts the observable to a blocking observable + + Observable> createDatabaseObservable = asyncClient + .createDatabase(databaseDefinition, null); + + // toBlocking() converts to a blocking observable + // single() gets the only result + createDatabaseObservable.toBlocking().single(); + } + + @Test + public void testCreateDatabase_toBlocking_DatabaseAlreadyExists_Fails() throws DocumentClientException { + + // attempt to create a database which already exists + // - first create a database + // - Using the async api generate an async database creation observable + // - Converts the Observable to blocking using Observable.toBlocking() api + // - catch already exist failure (409) + + asyncClient.createDatabase(databaseDefinition, null).toBlocking().single(); + + // Create the database for test. + Observable> databaseForTestObservable = asyncClient + .createDatabase(databaseDefinition, null); + + try { + databaseForTestObservable + .toBlocking() //blocks + .single(); //gets the single result + assertThat("Should not reach here", false); + } catch (Exception e) { + assertThat("Database already exists.", + ((DocumentClientException) e.getCause()).getStatusCode(), equalTo(409)); + } + } + + @Test + public void testTransformObservableToGoogleGuavaListenableFuture() throws Exception { + + // You can convert an Observable to a ListenableFuture. + // ListenableFuture (part of google guava library) is a popular extension + // of Java's Future which allows registering listener callbacks: + // https://github.com/google/guava/wiki/ListenableFutureExplained + + Observable> createDatabaseObservable = asyncClient.createDatabase(databaseDefinition, null); + ListenableFuture> future = ListenableFutureObservable.to(createDatabaseObservable); + + ResourceResponse rrd = future.get(); + + assertThat(rrd.getRequestCharge(), greaterThan((double) 0)); + System.out.print(rrd.getRequestCharge()); + } + + private void cleanUpGeneratedDatabases() throws DocumentClientException { + LOGGER.info("cleanup databases invoked"); + + String[] allDatabaseIds = { DATABASE_ID }; + + for (String id : allDatabaseIds) { + try { + List> feedResponsePages = asyncClient + .queryDatabases(new SqlQuerySpec("SELECT * FROM root r WHERE r.id=@id", + new SqlParameterCollection(new SqlParameter("@id", id))), null).toList().toBlocking().single(); + + + if (!feedResponsePages.get(0).getResults().isEmpty()) { + Database res = feedResponsePages.get(0).getResults().get(0); + LOGGER.info("deleting a database " + feedResponsePages.get(0)); + asyncClient.deleteDatabase(res.getSelfLink(), null).toBlocking().single(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/DocumentCRUDAsyncAPITest.java b/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/DocumentCRUDAsyncAPITest.java new file mode 100644 index 000000000000..fd9962c17117 --- /dev/null +++ b/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/DocumentCRUDAsyncAPITest.java @@ -0,0 +1,485 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.examples; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.util.concurrent.ListenableFuture; +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.SqlParameter; +import com.microsoft.azure.documentdb.SqlParameterCollection; +import com.microsoft.azure.documentdb.SqlQuerySpec; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; +import com.microsoft.azure.documentdb.rx.examples.TestConfigurations; + +import rx.Observable; +import rx.functions.Action1; +import rx.observable.ListenableFutureObservable; + +/** + * This integration test class demonstrates how to use Async API to create, + * delete, replace, and upsert. If you are interested in examples for querying + * for documents please see {@link DocumentQueryAsyncAPITest} + * + * NOTE: you can use rxJava based async api with java8 lambda expression. Using + * of rxJava based async APIs with java8 lambda expressions is much prettier. + * + * You can also use the async API without java8 lambda expression. + * + * For example + *
    + *
  • {@link #testCreateDocument_Async()} demonstrates how to use async api + * with java8 lambda expression. + * + *
  • {@link #testCreateDocument_Async_withoutLambda()} demonstrates how to the same + * thing without lambda expression. + *
+ * + * Also if you need to work with Future or ListenableFuture it is possible to + * transform an observable to ListenableFuture. Please see + * {@link #testTransformObservableToGoogleGuavaListenableFuture()} + * + */ +public class DocumentCRUDAsyncAPITest { + + private static final Logger LOGGER = LoggerFactory.getLogger(DocumentCRUDAsyncAPITest.class); + + private static final String DATABASE_ID = "async-test-db"; + + private AsyncDocumentClient asyncClient; + private DocumentCollection createdCollection; + + @Before + public void setUp() throws DocumentClientException { + + // sets up the requirements for each test + + asyncClient = new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(ConnectionPolicy.GetDefault()) + .withConsistencyLevel(ConsistencyLevel.Session) + .build(); + // Clean up the database. + this.cleanUpGeneratedDatabases(); + + Database databaseDefinition = new Database(); + databaseDefinition.setId(DATABASE_ID); + + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setId(UUID.randomUUID().toString()); + + // create database + ResourceResponse databaseCreationResponse = asyncClient.createDatabase(databaseDefinition, null) + .toBlocking().single(); + + // create collection + createdCollection = asyncClient + .createCollection(databaseCreationResponse.getResource().getSelfLink(), collectionDefinition, null) + .toBlocking().single().getResource(); + } + + @After + public void shutdown() throws DocumentClientException { + asyncClient.close(); + } + + @Test + public void testCreateDocument_Async() throws Exception { + + // create a document + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + Observable> createDocumentObservable = asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, true); + + final CountDownLatch doneLatch = new CountDownLatch(1); + + // subscribe to events emitted by the observable + createDocumentObservable + .single() // we know there will be one response + .subscribe( + + documentResourceResponse -> { + System.out.println(documentResourceResponse.getActivityId()); + doneLatch.countDown(); + }, + + error -> { + System.err.println("an error happened in document creation: actual cause: " + error.getMessage()); + }); + + // wait till document creation completes + doneLatch.await(); + } + + @Test + public void testCreateDocument_Async_withoutLambda() throws Exception { + + // create a document in without java8 lambda expressions + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + Observable> createDocumentObservable = asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, true); + + final CountDownLatch doneLatch = new CountDownLatch(1); + + Action1> onNext = new Action1>() { + + @Override + public void call(ResourceResponse documentResourceResponse) { + System.out.println(documentResourceResponse.getActivityId()); + doneLatch.countDown(); + } + }; + + Action1 onError = new Action1() { + + @Override + public void call(Throwable error) { + System.err.println("an error happened in document creation: actual cause: " + error.getMessage()); + } + }; + + // subscribe to events emitted by the observable + createDocumentObservable + .single() // we know there will be one response + .subscribe(onNext, onError); + + // wait till document creation completes + doneLatch.await(); + } + + @Test + public void testCreateDocument_toBlocking() throws DocumentClientException { + + // create a document + // toBlocking() converts the observable to a blocking observable + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + Observable> createDocumentObservable = + asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, true); + + + // toBlocking() converts to a blocking observable + // single() gets the only result + createDocumentObservable + .toBlocking() //converts the observable to a blocking observable + .single(); //gets the single result + } + + @Test + public void testDocumentCreation_SumUpRequestCharge() throws Exception { + + // create 10 documents and sum up all the documents creation request charges + + // create 10 documents + List>> listOfCreateDocumentObservables = new ArrayList<>(); + for(int i = 0; i < 10; i++) { + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", i, i)); + + Observable> createDocumentObservable = + asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, false); + listOfCreateDocumentObservables.add(createDocumentObservable); + } + + // merge all document creation observables into one observable + Observable> mergedObservable = Observable.merge(listOfCreateDocumentObservables); + + // create a new observable emitting the total charge of creating all 10 documents + Observable totalChargeObservable = mergedObservable + .map(ResourceResponse::getRequestCharge) //map to request charge + .reduce((totalCharge, charge) -> totalCharge + charge); //sum up all the charges + + final CountDownLatch doneLatch = new CountDownLatch(1); + + // subscribe to the total request charge observable + totalChargeObservable.subscribe(totalCharge -> { + // print the total charge + System.out.println(totalCharge); + doneLatch.countDown(); + }); + + doneLatch.await(); + } + + @Test + public void testCreateDocument_toBlocking_DocumentAlreadyExists_Fails() throws DocumentClientException { + + // attempt to create a document which already exists + // - first create a document + // - Using the async api generate an async document creation observable + // - Converts the Observable to blocking using Observable.toBlocking() api + // - catch already exist failure (409) + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, false).toBlocking().single(); + + // Create the document + Observable> createDocumentObservable = asyncClient + .createDocument(createdCollection.getSelfLink(), doc, null, false); + + try { + createDocumentObservable + .toBlocking() //converts the observable to a blocking observable + .single(); //gets the single result + Assert.fail("Document Already Exists. Document Creation must fail"); + } catch (Exception e) { + assertThat("Document already exists.", + ((DocumentClientException) e.getCause()).getStatusCode(), equalTo(409)); + } + } + + @Test + public void testCreateDocument_Async_DocumentAlreadyExists_Fails() throws Exception { + + // attempt to create a document which already exists + // - first create a document + // - Using the async api generate an async document creation observable + // - Converts the Observable to blocking using Observable.toBlocking() api + // - catch already exist failure (409) + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, false).toBlocking().single(); + + // Create the document + Observable> createDocumentObservable = asyncClient + .createDocument(createdCollection.getSelfLink(), doc, null, false); + + List errorList = Collections.synchronizedList(new ArrayList()); + + createDocumentObservable.subscribe( + resourceResponse -> {}, + + error -> { + errorList.add(error); + System.err.println("failed to create a document due to: " + error.getMessage()); + } + ); + + Thread.sleep(2000); + assertThat(errorList, hasSize(1)); + assertThat(errorList.get(0), is(instanceOf(DocumentClientException.class))); + assertThat(((DocumentClientException) errorList.get(0)).getStatusCode(), equalTo(409)); + } + + @Test + public void testDocumentReplace_Async() throws Exception { + + // replace a document + + // create a document + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + String documentLink = asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, false).toBlocking().single().getResource().getSelfLink(); + + // try to replace the existing document + Document replacingDocument = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d', 'new-prop' : '2'}", 1, 1)); + Observable> replaceDocumentObservable = asyncClient + .replaceDocument(documentLink, replacingDocument, null); + + List> capturedResponse = Collections.synchronizedList(new ArrayList>()); + + replaceDocumentObservable.subscribe( + resourceResponse -> { + capturedResponse.add(resourceResponse); + } + + ); + + Thread.sleep(2000); + + assertThat(capturedResponse, hasSize(1)); + assertThat(capturedResponse.get(0).getResource().get("new-prop"), equalTo("2")); + } + + @Test + public void testDocumentUpsert_Async() throws Exception { + + // upsert a document + + // create a document + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, false).toBlocking().single(); + + // upsert the existing document + Document upsertingDocument = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d', 'new-prop' : '2'}", 1, 1)); + Observable> upsertDocumentObservable = asyncClient + .upsertDocument(createdCollection.getSelfLink(), upsertingDocument, null, false); + + List> capturedResponse = Collections.synchronizedList(new ArrayList>()); + + upsertDocumentObservable.subscribe( + resourceResponse -> { + capturedResponse.add(resourceResponse); + } + + ); + + Thread.sleep(4000); + + assertThat(capturedResponse, hasSize(1)); + assertThat(capturedResponse.get(0).getResource().get("new-prop"), equalTo("2")); + } + + @Test + public void testDocumentDelete_Async() throws Exception { + + // delete a document + + // create a document + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + String documentLink = asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, false).toBlocking().single().getResource().getSelfLink(); + + // delete the existing document + Observable> deleteDocumentObservable = asyncClient + .deleteDocument(documentLink, null); + + List> capturedResponse = Collections.synchronizedList(new ArrayList>()); + + deleteDocumentObservable.subscribe( + resourceResponse -> { + capturedResponse.add(resourceResponse); + } + + ); + + Thread.sleep(2000); + + assertThat(capturedResponse, hasSize(1)); + + // assert document is deleted + List listOfDocuments = asyncClient + .queryDocuments(createdCollection.getSelfLink(), "SELECT * FROM root", null) + .map(FeedResponsePage::getResults) //map page to its list of documents + .concatMap(Observable::from) //flatten the observable + .toList() //transform to a observable + .toBlocking() //block + .single(); //gets the List + + // assert that there is no document found + assertThat(listOfDocuments, hasSize(0)); + } + + @Test + public void testDocumentRead_Async() throws Exception { + + // read a document + + //create a document + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + String documentLink = asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, false).toBlocking().single().getResource().getSelfLink(); + + // read the document + Observable> readDocumentObservable = asyncClient + .readDocument(documentLink, null); + + List> capturedResponse = Collections.synchronizedList(new ArrayList>()); + + readDocumentObservable.subscribe( + resourceResponse -> { + capturedResponse.add(resourceResponse); + } + + ); + + Thread.sleep(2000); + + // assert document is retrieved + assertThat(capturedResponse, hasSize(1)); + } + + @Test + public void testTransformObservableToFuture() throws Exception { + + // You can convert an Observable to a Future. + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + Observable> createDocumentObservable = asyncClient + .createDocument(createdCollection.getSelfLink(), doc, null, false); + Future> future = createDocumentObservable.toBlocking().toFuture(); + + ResourceResponse rrd = future.get(); + + assertThat(rrd.getRequestCharge(), greaterThan((double) 0)); + System.out.print(rrd.getRequestCharge()); + } + + @Test + public void testTransformObservableToGoogleGuavaListenableFuture() throws Exception { + + // You can convert an Observable to a ListenableFuture. + // ListenableFuture (part of google guava library) is a popular extension + // of Java's Future which allows registering listener callbacks: + // https://github.com/google/guava/wiki/ListenableFutureExplained + Document doc = new Document(String.format("{ 'id': 'doc%d', 'counter': '%d'}", 1, 1)); + Observable> createDocumentObservable = asyncClient + .createDocument(createdCollection.getSelfLink(), doc, null, false); + ListenableFuture> listenableFuture = ListenableFutureObservable.to(createDocumentObservable); + + ResourceResponse rrd = listenableFuture.get(); + + assertThat(rrd.getRequestCharge(), greaterThan((double) 0)); + System.out.print(rrd.getRequestCharge()); + } + + private void cleanUpGeneratedDatabases() throws DocumentClientException { + LOGGER.info("cleanup databases invoked"); + + String[] allDatabaseIds = { DATABASE_ID }; + + for (String id : allDatabaseIds) { + try { + List> feedResponsePages = asyncClient + .queryDatabases(new SqlQuerySpec("SELECT * FROM root r WHERE r.id=@id", + new SqlParameterCollection(new SqlParameter("@id", id))), null).toList().toBlocking().single(); + + + if (!feedResponsePages.get(0).getResults().isEmpty()) { + Database res = feedResponsePages.get(0).getResults().get(0); + LOGGER.info("deleting a database " + feedResponsePages.get(0)); + asyncClient.deleteDatabase(res.getSelfLink(), null).toBlocking().single(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/DocumentQueryAsyncAPITest.java b/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/DocumentQueryAsyncAPITest.java new file mode 100644 index 000000000000..0802156d5742 --- /dev/null +++ b/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/DocumentQueryAsyncAPITest.java @@ -0,0 +1,516 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.examples; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.notNullValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.util.concurrent.ListenableFuture; +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.FeedOptions; +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.PartitionKeyDefinition; +import com.microsoft.azure.documentdb.RequestOptions; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.SqlParameter; +import com.microsoft.azure.documentdb.SqlParameterCollection; +import com.microsoft.azure.documentdb.SqlQuerySpec; +import com.microsoft.azure.documentdb.internal.HttpConstants; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; +import com.microsoft.azure.documentdb.rx.examples.TestConfigurations; + +import rx.Observable; +import rx.Subscriber; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.observable.ListenableFutureObservable; + +/** + * This integration test class demonstrates how to use Async API to query for + * documents. + * + * NOTE: you can use rxJava based async api with java8 lambda expression. Using of + * rxJava based async APIs with java8 lambda expressions is much prettier. + * + * You can also use the async API without java8 lambda expression. + * + * For example + *
    + *
  • {@link #testQueryDocuments_Async()} demonstrates how to use async api + * with java8 lambda expression. + * + *
  • {@link #testQueryDocuments_Async_withoutLambda()} demonstrates how to the same + * thing without lambda expression. + *
+ * + * Also if you need to work with Future or ListenableFuture it is possible to transform + * an observable to ListenableFuture. Please see {@link #testTransformObservableToGoogleGuavaListenableFuture()} + * + */ +public class DocumentQueryAsyncAPITest { + + private static final Logger LOGGER = LoggerFactory.getLogger(DocumentQueryAsyncAPITest.class); + + private static final String DATABASE_ID = "async-test-db"; + + private AsyncDocumentClient asyncClient; + + private DocumentCollection createdCollection; + private Database createdDatabase; + + private int numberOfDocuments; + + @Before + public void setUp() throws DocumentClientException { + + asyncClient = new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(ConnectionPolicy.GetDefault()) + .withConsistencyLevel(ConsistencyLevel.Session) + .build(); + + // Clean up the database. + this.cleanUpGeneratedDatabases(); + + Database databaseDefinition = new Database(); + databaseDefinition.setId(DATABASE_ID); + + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setId(UUID.randomUUID().toString()); + + // create database + ResourceResponse databaseCreationResponse = asyncClient.createDatabase(databaseDefinition, null) + .toBlocking().single(); + + createdDatabase = databaseCreationResponse.getResource(); + + // create collection + createdCollection = asyncClient + .createCollection(databaseCreationResponse.getResource().getSelfLink(), collectionDefinition, null) + .toBlocking().single().getResource(); + + numberOfDocuments = 20; + // add documents + for (int i = 0; i < numberOfDocuments; i++) { + Document doc = new Document(String.format("{ 'id': 'loc%d', 'counter': %d}", i, i)); + asyncClient.createDocument(createdCollection.getSelfLink(), doc, null, true).toBlocking().single(); + } + } + + @After + public void shutdown() throws DocumentClientException { + asyncClient.close(); + } + + @Test + public void testQueryDocuments_Async() throws Exception { + + // query for documents + // creates a document query observable and verifies the async behavior + // of document query observable + + int requestPageSize = 3; + FeedOptions options = new FeedOptions(); + options.setPageSize(requestPageSize); + + Observable> documentQueryObservable = asyncClient + .queryDocuments(createdCollection.getSelfLink(), "SELECT * FROM root", options); + + final CountDownLatch mainThreadBarrier = new CountDownLatch(1); + + final CountDownLatch resultsCountDown = new CountDownLatch(numberOfDocuments); + + // forEach(.) is an alias for subscribe(.) + + documentQueryObservable.forEach(page -> { + try { + // waits on the barrier + mainThreadBarrier.await(); + } catch (InterruptedException e) { + } + + for (@SuppressWarnings("unused") Document d : page.getResults()) { + resultsCountDown.countDown(); + } + }); + + // The following code will run concurrently + + System.out.println("action is subscribed to the observable"); + + // release main thread barrier + System.out.println("after main thread barrier is released, subscribed observable action can continue"); + mainThreadBarrier.countDown(); + + System.out.println("waiting for all the results using result count down latch"); + + resultsCountDown.await(); + } + + @Test + public void testQueryDocuments_Async_withoutLambda() throws Exception { + + // query for documents + // creates a document query observable and verifies the async behavior + // of document query observable + + // NOTE: does the same thing as testQueryDocuments_Async without java8 lambda expression + + int requestPageSize = 3; + FeedOptions options = new FeedOptions(); + options.setPageSize(requestPageSize); + + Observable> documentQueryObservable = asyncClient + .queryDocuments(createdCollection.getSelfLink(), "SELECT * FROM root", options); + + final CountDownLatch mainThreadBarrier = new CountDownLatch(1); + + final CountDownLatch resultsCountDown = new CountDownLatch(numberOfDocuments); + + Action1> actionPerPage = new Action1>() { + + @SuppressWarnings("unused") + @Override + public void call(FeedResponsePage t) { + + try { + // waits on the barrier + mainThreadBarrier.await(); + } catch (InterruptedException e) { + } + + for (Document d : t.getResults()) { + resultsCountDown.countDown(); + } + } + }; + + // forEach(.) is an alias for subscribe(.) + documentQueryObservable.forEach(actionPerPage); + // the following code will run concurrently + + System.out.println("action is subscribed to the observable"); + + // release main thread barrier + System.out.println("after main thread barrier is released, subscribed observable action can continue"); + mainThreadBarrier.countDown(); + + System.out.println("waiting for all the results using result count down latch"); + + resultsCountDown.await(); + } + + @Test + public void testQueryDocuments_findTotalRequestCharge() throws Exception { + + // queries for documents and sum up the total request charge + + int requestPageSize = 3; + FeedOptions options = new FeedOptions(); + options.setPageSize(requestPageSize); + + Observable totalChargeObservable = asyncClient + .queryDocuments(createdCollection.getSelfLink(), "SELECT * FROM root", options) + .map(FeedResponsePage::getRequestCharge) // map the page to its request charge + .reduce((totalCharge, charge) -> totalCharge + charge); // sum up all the request charges + + final CountDownLatch doneLatch = new CountDownLatch(1); + + // subscribe(.) is the same as forEach(.) + totalChargeObservable.subscribe(totalCharge -> { + System.out.println(totalCharge); + doneLatch.countDown(); + }); + + doneLatch.await(); + } + + @Test + public void testQueryDocuments_unsubscribeAfterFirstPage() throws Exception { + + // subscriber unsubscribes after first page + + int requestPageSize = 3; + FeedOptions options = new FeedOptions(); + options.setPageSize(requestPageSize); + + Observable> requestChargeObservable = asyncClient + .queryDocuments(createdCollection.getSelfLink(), "SELECT * FROM root", options); + + AtomicInteger onNextCounter = new AtomicInteger(); + AtomicInteger onCompletedCounter = new AtomicInteger(); + AtomicInteger onErrorCounter = new AtomicInteger(); + + requestChargeObservable.subscribe(new Subscriber>() { + + @Override + public void onCompleted() { + onCompletedCounter.incrementAndGet(); + } + + @Override + public void onError(Throwable e) { + onErrorCounter.incrementAndGet(); + } + + @Override + public void onNext(FeedResponsePage page) { + onNextCounter.incrementAndGet(); + unsubscribe(); + } + }); + + Thread.sleep(4000); + + // after subscriber unsubscribes, it doesn't receive any more events. + assertThat(onNextCounter.get(), equalTo(1)); + assertThat(onCompletedCounter.get(), equalTo(0)); + assertThat(onErrorCounter.get(), equalTo(0)); + } + + @Test + public void testQueryDocuments_filterFetchedResults() throws Exception { + // queries for documents and filter out the fetched results + + int requestPageSize = 3; + FeedOptions options = new FeedOptions(); + options.setPageSize(requestPageSize); + + Func1 isPrimeNumber = new Func1() { + + @Override + public Boolean call(Document doc) { + int n = doc.getInt("counter"); + if (n <= 1) return false; + for(int i = 2; 2*i < n; i++) { + if(n % i == 0) + return false; + } + return true; + } + }; + + List resultList = Collections.synchronizedList(new ArrayList()); + + asyncClient + .queryDocuments(createdCollection.getSelfLink(), "SELECT * FROM root", options) + .map(FeedResponsePage::getResults) // map the page to the list of documents + .concatMap(Observable::from) // flatten the observable> to observable + .filter(isPrimeNumber) // filter documents using isPrimeNumber predicate + .subscribe(doc -> resultList.add(doc)); // collect the results + + Thread.sleep(4000); + + int expectedNumberOfPrimes = 0; + // find all the documents with prime number counter + for(int i = 0; i < numberOfDocuments; i++) { + boolean isPrime = true; + if (i <= 1) isPrime = false; + for(int j = 2; 2*j < i; j++) { + if(i % j == 0) { + isPrime = false; + break; + } + } + + if (isPrime) { + expectedNumberOfPrimes++; + } + } + + // assert that we only collected what's expected + assertThat(resultList, hasSize(expectedNumberOfPrimes)); + } + + @Test + public void testQueryDocuments_toBlocking_toIterator() throws DocumentClientException { + + // queries for documents + // converts the document query observable to blocking observable and + // uses that to find all documents + + // query for documents + int requestPageSize = 3; + FeedOptions options = new FeedOptions(); + options.setPageSize(requestPageSize); + + Observable> documentQueryObservable = asyncClient + .queryDocuments(createdCollection.getSelfLink(), "SELECT * FROM root", options); + + // covert the observable to a blocking observable, then convert the blocking observable to an iterator + Iterator> it = documentQueryObservable.toBlocking().getIterator(); + + int pageCounter = 0; + int numberOfResults = 0; + while (it.hasNext()) { + FeedResponsePage page = it.next(); + pageCounter++; + + String pageSizeAsString = page.getResponseHeaders().get(HttpConstants.HttpHeaders.ITEM_COUNT); + assertThat("header item count must be present", pageSizeAsString, notNullValue()); + int pageSize = Integer.valueOf(pageSizeAsString); + assertThat("Result size must match header item count", page.getResults(), hasSize(pageSize)); + numberOfResults += pageSize; + } + assertThat("number of total results", numberOfResults, equalTo(numberOfDocuments)); + assertThat("number of result pages", pageCounter, + equalTo((numberOfDocuments + requestPageSize - 1) / requestPageSize)); + } + + @Test + public void testOrderBy_Async() throws Exception { + // create a partitioned collection + String collectionId = UUID.randomUUID().toString(); + DocumentCollection multiPartitionCollection = createMultiPartitionCollection(createdDatabase.getSelfLink(), collectionId, "/key"); + + // insert documents + int totalNumberOfDocumentsInMultiPartitionCollection = 10; + for (int i = 0; i < totalNumberOfDocumentsInMultiPartitionCollection; i++) { + + Document doc = new Document(String.format( "{\"id\":\"documentId%d\",\"key\":\"%s\",\"prop\":%d}", + i, RandomStringUtils.randomAlphabetic(2), i)); + asyncClient.createDocument(multiPartitionCollection.getSelfLink(), doc, null, true).toBlocking().single(); + } + + // query for the documents order by the prop field + SqlQuerySpec query = new SqlQuerySpec("SELECT r.id FROM r ORDER BY r.prop", new SqlParameterCollection()); + FeedOptions options = new FeedOptions(); + options.setEnableCrossPartitionQuery(true); + options.setPageSize(1); + + // get the observable order by query documents + Observable> documentQueryObservable = asyncClient + .queryDocuments(multiPartitionCollection.getSelfLink(), query, options); + + List resultList = (List) Collections.synchronizedList(new ArrayList()); + + documentQueryObservable + .map(FeedResponsePage::getResults) // map the logical page to the list of documents in the page + .concatMap(Observable::from) // flatten the list of documents + .map(doc -> doc.getId()) // map to the document Id + .forEach(docId -> resultList.add(docId)); // add each document Id to the resultList + + Thread.sleep(4000); + + // assert we found all the results + assertThat(resultList, hasSize(totalNumberOfDocumentsInMultiPartitionCollection)); + for(int i = 0; i < totalNumberOfDocumentsInMultiPartitionCollection; i++) { + String docId = resultList.get(i); + // assert that the order of the documents are valid + assertThat(docId, equalTo("documentId" + i)); + } + } + + @Test + public void testTransformObservableToGoogleGuavaListenableFuture() throws Exception { + // You can convert an Observable to a ListenableFuture. + // ListenableFuture (part of google guava library) is a popular extension + // of Java's Future which allows registering listener callbacks: + // https://github.com/google/guava/wiki/ListenableFutureExplained + + int requestPageSize = 3; + FeedOptions options = new FeedOptions(); + options.setPageSize(requestPageSize); + + Observable> documentQueryObservable = asyncClient + .queryDocuments(createdCollection.getSelfLink(), "SELECT * FROM root", options); + + // convert to observable of list of pages + Observable>> allPagesObservable = documentQueryObservable.toList(); + + // convert the observable of list of pages to a Future + ListenableFuture>> future = ListenableFutureObservable.to(allPagesObservable); + + List> pageList = future.get(); + + int totalNumberOfRetrievedDocuments = 0; + for(FeedResponsePage page: pageList) { + totalNumberOfRetrievedDocuments += page.getResults().size(); + } + assertThat(numberOfDocuments, equalTo(totalNumberOfRetrievedDocuments)); + } + + private void cleanUpGeneratedDatabases() throws DocumentClientException { + LOGGER.info("cleanup databases invoked"); + + String[] allDatabaseIds = { DATABASE_ID }; + + for (String id : allDatabaseIds) { + try { + List> feedResponsePages = asyncClient + .queryDatabases(new SqlQuerySpec("SELECT * FROM root r WHERE r.id=@id", + new SqlParameterCollection(new SqlParameter("@id", id))), null) + .toList().toBlocking().single(); + + if (!feedResponsePages.get(0).getResults().isEmpty()) { + Database res = feedResponsePages.get(0).getResults().get(0); + LOGGER.info("deleting a database " + feedResponsePages.get(0)); + asyncClient.deleteDatabase(res.getSelfLink(), null).toBlocking().single(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private DocumentCollection createMultiPartitionCollection(String databaseLink, String collectionId, + String partitionKeyPath) throws DocumentClientException { + PartitionKeyDefinition partitionKeyDef = new PartitionKeyDefinition(); + ArrayList paths = new ArrayList(); + paths.add(partitionKeyPath); + partitionKeyDef.setPaths(paths); + + RequestOptions options = new RequestOptions(); + options.setOfferThroughput(10100); + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setId(collectionId); + collectionDefinition.setPartitionKey(partitionKeyDef); + DocumentCollection createdCollection = asyncClient + .createCollection(databaseLink, collectionDefinition, options).toBlocking().single().getResource(); + + return createdCollection; + } +} diff --git a/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/TestConfigurations.java b/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/TestConfigurations.java new file mode 100644 index 000000000000..15d8605b8dfc --- /dev/null +++ b/azure-documentdb-examples/src/test/java/com/microsoft/azure/documentdb/rx/examples/TestConfigurations.java @@ -0,0 +1,35 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.examples; + +/** + * Contains the configurations for test file + */ +public final class TestConfigurations { + // Replace MASTER_KEY and HOST with values from your DocumentDB account. + // The default values are credentials of the local emulator, which are not used in any production environment. + // + public static final String MASTER_KEY = + "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + public static final String HOST = "https://localhost:443/"; +} diff --git a/azure-documentdb-examples/src/test/resources/log4j.properties b/azure-documentdb-examples/src/test/resources/log4j.properties new file mode 100644 index 000000000000..7a8ae3aef1e5 --- /dev/null +++ b/azure-documentdb-examples/src/test/resources/log4j.properties @@ -0,0 +1,17 @@ +# this is the log4j configuration for tests + +# Set root logger level to DEBUG and its only appender to A1. +log4j.rootLogger=INFO, A1 + +# Set HTTP components' logger to INFO +log4j.category.org.apache.http=WARN +log4j.category.org.apache.http.wire=WARN +log4j.category.org.apache.http.headers=WARN +log4j.category.com.microsoft.azure.documentdb.internal.ServiceJNIWrapper=ERROR + +# A1 is set to be a ConsoleAppender. +log4j.appender.A1=org.apache.log4j.ConsoleAppender + +# A1 uses PatternLayout. +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%d %5X{pid} [%t] %-5p %c - %m%n \ No newline at end of file diff --git a/azure-documentdb-rx/pom.xml b/azure-documentdb-rx/pom.xml new file mode 100644 index 000000000000..4c5d417268ea --- /dev/null +++ b/azure-documentdb-rx/pom.xml @@ -0,0 +1,302 @@ + + 4.0.0 + + com.microsoft.azure + azure-documentdb-rx + 0.9.0-SNAPSHOT + jar + + azure-documentdb-rx + Java Reactive Extension (Rx) for Microsoft Azure DocumentDB SDK + http://azure.microsoft.com/en-us/services/documentdb/ + + + MIT License + http://www.opensource.org/licenses/mit-license.php + + + + UTF-8 + 1.7.6 + 1.2.17 + 4.1.7.Final + 0.4.20 + 1.2.5 + 6.8.8 + 1.7.0 + 1.10.8 + unit + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.8 + + + + org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.19.1 + + ${test.groups} + + + + org.apache.maven.surefire + surefire-testng + 2.19.1 + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.19.1 + + ${test.groups} + + + + maven-assembly-plugin + 2.2 + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.3 + true + + ossrh + https://oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + http://reactivex.io/RxJava/javadoc/ + http://azure.github.io/azure-documentdb-java/ + + + **/internal/**/*.java + **/*Internal.java + + + + + attach-javadocs + + jar + + + + + + + + + default + + default + unit + + + true + + + + fast + + default + unit,simple + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.19.1 + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.4 + + + org.apache.maven.plugins + maven-jxr-plugin + 2.1 + + + + + + commons-io + commons-io + 2.5 + + + io.reactivex + rxjava + ${rxjava.version} + + + io.reactivex + rxjava-string + 1.1.1 + + + com.microsoft.azure + azure-documentdb + 1.9.7-SNAPSHOT + + + io.reactivex + rxnetty + ${rxnetty.version} + + + io.reactivex + rxnetty-servo + ${rxnetty.version} + + + io.netty + netty-codec-http + ${netty.version} + + + io.netty + netty-handler + ${netty.version} + + + io.netty + netty-transport + ${netty.version} + + + io.netty + netty-transport-native-epoll + ${netty.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.apache.commons + commons-lang3 + 3.5 + + + org.testng + testng + ${testng.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.mockito + mockito-all + ${mockito.version} + test + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + test + + + log4j + log4j + ${log4j.version} + test + + + + + DocumentDB Developer Platform Devs + docdbdevplatdevs@microsoft.com + Microsoft + http://www.microsoft.com/ + + + + scm:git:git@github.com:Azure/azure-documentdb-java.git + scm:git:git@github.com:Azure/azure-documentdb-java.git + git@github.com:Azure/azure-documentdb-java.git + + diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/BridgeInternal.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/BridgeInternal.java new file mode 100644 index 000000000000..9531b542b928 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/BridgeInternal.java @@ -0,0 +1,105 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb; + +import java.net.URI; +import java.util.Map; +import java.util.concurrent.ExecutorService; + +import com.microsoft.azure.documentdb.internal.AbstractDocumentServiceRequest; +import com.microsoft.azure.documentdb.internal.CollectionCacheInternal; +import com.microsoft.azure.documentdb.internal.DocumentServiceResponse; +import com.microsoft.azure.documentdb.internal.EndpointManager; +import com.microsoft.azure.documentdb.internal.UserAgentContainer; +import com.microsoft.azure.documentdb.internal.routing.ClientCollectionCache; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; +import com.microsoft.azure.documentdb.rx.internal.Constants; +import com.microsoft.azure.documentdb.rx.internal.RxDocumentClientImpl; + +/** + * This is meant to be used only internally as a bridge access to + * classes in com.microsoft.azure.documentdb + **/ +public class BridgeInternal { + + public static Document documentFromObject(Object document) { + return Document.FromObject(document); + } + + public static DocumentClient createDocumentClient(String serviceEndpoint, String masterKey, ConnectionPolicy connectionPolicy, ConsistencyLevel consistencyLevel) { + return new DocumentClient(serviceEndpoint, masterKey, connectionPolicy, consistencyLevel, null, null, + new UserAgentContainer(Constants.Versions.SDK_NAME, Constants.Versions.SDK_VERSION)); + } + + public static ResourceResponse toResourceResponse(DocumentServiceResponse response, Class cls) { + return new ResourceResponse(response, cls); + } + + public static void validateResource(Resource resource){ + DocumentClient.validateResource(resource); + } + + public static void addPartitionKeyInformation(AbstractDocumentServiceRequest request, + Document document, + RequestOptions options, DocumentCollection collection){ + DocumentClient.addPartitionKeyInformation(request, document, options, collection); + } + + public static ClientCollectionCache createClientCollectionCache(AsyncDocumentClient asyncClient, ExecutorService executorService) { + CollectionCacheInternal collectionReader = new CollectionCacheInternal() { + + @Override + public ResourceResponse readCollection(String collectionLink, RequestOptions options) + throws DocumentClientException { + return asyncClient.readCollection(collectionLink, options).toBlocking().single(); + } + }; + return new ClientCollectionCache(collectionReader, executorService); + } + + public static Map getRequestHeaders(RequestOptions options) { + return DocumentClient.getRequestHeaders(options); + } + + public static EndpointManager createGlobalEndpointManager(RxDocumentClientImpl asyncClient) { + + DatabaseAccountManagerInternal databaseAccountManager = new DatabaseAccountManagerInternal() { + + @Override + public URI getServiceEndpoint() { + return asyncClient.getServiceEndpoint(); + } + + @Override + public DatabaseAccount getDatabaseAccountFromEndpoint(URI endpoint) throws DocumentClientException { + return asyncClient.getDatabaseAccountFromEndpoint(endpoint).toBlocking().single(); + } + + @Override + public ConnectionPolicy getConnectionPolicy() { + return asyncClient.getConnectionPolicy(); + } + }; + return new GlobalEndpointManager(databaseAccountManager); + } +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/FeedResponsePage.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/FeedResponsePage.java new file mode 100644 index 000000000000..b1c8386eaa45 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/FeedResponsePage.java @@ -0,0 +1,313 @@ +package com.microsoft.azure.documentdb; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import com.microsoft.azure.documentdb.internal.Constants; +import com.microsoft.azure.documentdb.internal.HttpConstants; + +public class FeedResponsePage { + + private final List results; + private final Map header; + private final HashMap usageHeaders; + private final HashMap quotaHeaders; + + public FeedResponsePage(List results, Map header) { + this.results = results; + this.header = header; + this.usageHeaders = new HashMap(); + this.quotaHeaders = new HashMap(); + } + + public List getResults() { + return results; + } + + /** + * Max Quota. + * + * @return the database quota. + */ + public long getDatabaseQuota() { + return this.getMaxQuotaHeader(Constants.Quota.DATABASE); + } + + /** + * Current Usage. + * + * @return the current database usage. + */ + public long getDatabaseUsage() { + return this.getCurrentQuotaHeader(Constants.Quota.DATABASE); + } + + /** + * Max Quota. + * + * @return the collection quota. + */ + public long getCollectionQuota() { + return this.getMaxQuotaHeader(Constants.Quota.COLLECTION); + } + + /** + * Current Usage. + * + * @return the current collection usage. + */ + public long getCollectionUsage() { + return this.getCurrentQuotaHeader(Constants.Quota.COLLECTION); + } + + /** + * Max Quota. + * + * @return the user quota. + */ + public long getUserQuota() { + return this.getMaxQuotaHeader(Constants.Quota.USER); + } + + /** + * Current Usage. + * + * @return the current user usage. + */ + public long getUserUsage() { + return this.getCurrentQuotaHeader(Constants.Quota.USER); + } + + /** + * Max Quota. + * + * @return the permission quota. + */ + public long getPermissionQuota() { + return this.getMaxQuotaHeader(Constants.Quota.PERMISSION); + } + + /** + * Current Usage. + * + * @return the current permission usage. + */ + public long getPermissionUsage() { + return this.getCurrentQuotaHeader(Constants.Quota.PERMISSION); + } + + /** + * Max Quota. + * + * @return the collection size quota. + */ + public long getCollectionSizeQuota() { + return this.getMaxQuotaHeader(Constants.Quota.COLLECTION_SIZE); + } + + /** + * Current Usage. + * + * @return the current collection size usage. + */ + public long getCollectionSizeUsage() { + return this.getCurrentQuotaHeader(Constants.Quota.COLLECTION_SIZE); + } + + /** + * Max Quota. + * + * @return the stored procedure quota. + */ + public long getStoredProceduresQuota() { + return this.getMaxQuotaHeader(Constants.Quota.STORED_PROCEDURE); + } + + /** + * Current Usage. + * + * @return the current stored procedure usage. + */ + public long getStoredProceduresUsage() { + return this.getCurrentQuotaHeader(Constants.Quota.STORED_PROCEDURE); + } + + /** + * Max Quota. + * + * @return the triggers quota. + */ + public long getTriggersQuota() { + return this.getMaxQuotaHeader(Constants.Quota.TRIGGER); + } + + /** + * Current Usage. + * + * @return the current triggers usage. + */ + public long getTriggersUsage() { + return this.getCurrentQuotaHeader(Constants.Quota.TRIGGER); + } + + /** + * Max Quota. + * + * @return the user defined functions quota. + */ + public long getUserDefinedFunctionsQuota() { + return this.getMaxQuotaHeader(Constants.Quota.USER_DEFINED_FUNCTION); + } + + /** + * Current Usage. + * + * @return the current user defined functions usage. + */ + public long getUserDefinedFunctionsUsage() { + return this.getCurrentQuotaHeader(Constants.Quota.USER_DEFINED_FUNCTION); + } + + /** + * Gets the maximum size limit for this entity (in megabytes (MB) for server resources and in count for master + * resources). + * + * @return the max resource quota. + */ + public String getMaxResourceQuota() { + return getValueOrNull(header, + HttpConstants.HttpHeaders.MAX_RESOURCE_QUOTA); + } + + /** + * Gets the current size of this entity (in megabytes (MB) for server resources and in count for master resources). + * + * @return the current resource quota usage. + */ + public String getCurrentResourceQuotaUsage() { + return getValueOrNull(header, + HttpConstants.HttpHeaders.CURRENT_RESOURCE_QUOTA_USAGE); + } + + /** + * Gets the number of index paths (terms) generated by the operation. + * + * @return the request charge. + */ + public double getRequestCharge() { + String value = getValueOrNull(header, + HttpConstants.HttpHeaders.REQUEST_CHARGE); + if (StringUtils.isEmpty(value)) { + return 0; + } + return Double.valueOf(value); + } + + /** + * Gets the activity ID for the request. + * + * @return the activity id. + */ + public String getActivityId() { + return getValueOrNull(header, HttpConstants.HttpHeaders.ACTIVITY_ID); + } + + /** + * Gets the continuation token to be used for continuing the enumeration. + * + * @return the response continuation. + */ + public String getResponseContinuation() { + return getValueOrNull(header, HttpConstants.HttpHeaders.CONTINUATION); + } + + /** + * Gets the session token for use in session consistency. + * + * @return the session token. + */ + public String getSessionToken() { + return getValueOrNull(header, HttpConstants.HttpHeaders.SESSION_TOKEN); + } + + /** + * Gets the response headers. + * + * @return the response headers. + */ + public Map getResponseHeaders() { + return header; + } + + private long getCurrentQuotaHeader(String headerName) { + if (this.usageHeaders.size() == 0 && !this.getMaxResourceQuota().isEmpty() && + !this.getCurrentResourceQuotaUsage().isEmpty()) { + this.populateQuotaHeader(this.getMaxResourceQuota(), this.getCurrentResourceQuotaUsage()); + } + + if (this.usageHeaders.containsKey(headerName)) { + return this.usageHeaders.get(headerName); + } + + return 0; + } + + private long getMaxQuotaHeader(String headerName) { + if (this.quotaHeaders.size() == 0 && + !this.getMaxResourceQuota().isEmpty() && + !this.getCurrentResourceQuotaUsage().isEmpty()) { + this.populateQuotaHeader(this.getMaxResourceQuota(), this.getCurrentResourceQuotaUsage()); + } + + if (this.quotaHeaders.containsKey(headerName)) { + return this.quotaHeaders.get(headerName); + } + + return 0; + } + + private void populateQuotaHeader(String headerMaxQuota, + String headerCurrentUsage) { + String[] headerMaxQuotaWords = headerMaxQuota.split(Constants.Quota.DELIMITER_CHARS, -1); + String[] headerCurrentUsageWords = headerCurrentUsage.split(Constants.Quota.DELIMITER_CHARS, -1); + + for (int i = 0; i < headerMaxQuotaWords.length; ++i) { + if (headerMaxQuotaWords[i].equalsIgnoreCase(Constants.Quota.DATABASE)) { + this.quotaHeaders.put(Constants.Quota.DATABASE, Long.valueOf(headerMaxQuotaWords[i + 1])); + this.usageHeaders.put(Constants.Quota.DATABASE, Long.valueOf(headerCurrentUsageWords[i + 1])); + } else if (headerMaxQuotaWords[i].equalsIgnoreCase(Constants.Quota.COLLECTION)) { + this.quotaHeaders.put(Constants.Quota.COLLECTION, Long.valueOf(headerMaxQuotaWords[i + 1])); + this.usageHeaders.put(Constants.Quota.COLLECTION, Long.valueOf(headerCurrentUsageWords[i + 1])); + } else if (headerMaxQuotaWords[i].equalsIgnoreCase(Constants.Quota.USER)) { + this.quotaHeaders.put(Constants.Quota.USER, Long.valueOf(headerMaxQuotaWords[i + 1])); + this.usageHeaders.put(Constants.Quota.USER, Long.valueOf(headerCurrentUsageWords[i + 1])); + } else if (headerMaxQuotaWords[i].equalsIgnoreCase(Constants.Quota.PERMISSION)) { + this.quotaHeaders.put(Constants.Quota.PERMISSION, Long.valueOf(headerMaxQuotaWords[i + 1])); + this.usageHeaders.put(Constants.Quota.PERMISSION, Long.valueOf(headerCurrentUsageWords[i + 1])); + } else if (headerMaxQuotaWords[i].equalsIgnoreCase(Constants.Quota.COLLECTION_SIZE)) { + this.quotaHeaders.put(Constants.Quota.COLLECTION_SIZE, Long.valueOf(headerMaxQuotaWords[i + 1])); + this.usageHeaders.put(Constants.Quota.COLLECTION_SIZE, Long.valueOf(headerCurrentUsageWords[i + 1])); + } else if (headerMaxQuotaWords[i].equalsIgnoreCase(Constants.Quota.STORED_PROCEDURE)) { + this.quotaHeaders.put(Constants.Quota.STORED_PROCEDURE, Long.valueOf(headerMaxQuotaWords[i + 1])); + this.usageHeaders.put(Constants.Quota.STORED_PROCEDURE, Long.valueOf(headerCurrentUsageWords[i + 1])); + } else if (headerMaxQuotaWords[i].equalsIgnoreCase(Constants.Quota.TRIGGER)) { + this.quotaHeaders.put(Constants.Quota.TRIGGER, Long.valueOf(headerMaxQuotaWords[i + 1])); + this.usageHeaders.put(Constants.Quota.TRIGGER, Long.valueOf(headerCurrentUsageWords[i + 1])); + } else if (headerMaxQuotaWords[i].equalsIgnoreCase(Constants.Quota.USER_DEFINED_FUNCTION)) { + this.quotaHeaders.put(Constants.Quota.USER_DEFINED_FUNCTION, Long.valueOf(headerMaxQuotaWords[i + 1])); + this.usageHeaders.put(Constants.Quota.USER_DEFINED_FUNCTION, + Long.valueOf(headerCurrentUsageWords[i + 1])); + } + } + } + + private static String getValueOrNull(Map map, String key) { + if (map != null) { + return map.get(key); + } + return null; + } +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/internal/RetryPolicyBridgeInternal.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/internal/RetryPolicyBridgeInternal.java new file mode 100644 index 000000000000..3a7877ae60a8 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/internal/RetryPolicyBridgeInternal.java @@ -0,0 +1,51 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.internal; + +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.internal.routing.ClientCollectionCache; + +/** + * This is meant to be used only internally as a bridge access to + * classes in com.microsoft.azure.documentdb.internal + **/ +public class RetryPolicyBridgeInternal { + + public static RetryPolicy createSessionReadRetryPolicy(EndpointManager globalEndpointManager, AbstractDocumentServiceRequest request) { + return new SessionReadRetryPolicy(globalEndpointManager, request); + } + + public static RetryPolicy createEndpointDiscoveryRetryPolicy(ConnectionPolicy connectionPolicy, EndpointManager globalEndpointManager) { + return new EndpointDiscoveryRetryPolicy(connectionPolicy, globalEndpointManager); + } + + public static RetryPolicy createResourceThrottleRetryPolicy(int maxRetryAttemptsOnThrottledRequests, + int maxRetryWaitTimeInSeconds) { + return new ResourceThrottleRetryPolicy(maxRetryAttemptsOnThrottledRequests, maxRetryWaitTimeInSeconds); + } + + public static RetryPolicy createPartitionKeyMismatchRetryPolicy(String resourcePath, + ClientCollectionCache clientCollectionCache) { + return new PartitionKeyMismatchRetryPolicy(resourcePath, clientCollectionCache); + } +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/AsyncDocumentClient.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/AsyncDocumentClient.java new file mode 100644 index 000000000000..64384bfa49ab --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/AsyncDocumentClient.java @@ -0,0 +1,1429 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; + +import com.microsoft.azure.documentdb.Attachment; +import com.microsoft.azure.documentdb.Conflict; +import com.microsoft.azure.documentdb.ConnectionMode; +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.DatabaseAccount; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClient; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.FeedOptions; +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.MediaOptions; +import com.microsoft.azure.documentdb.MediaResponse; +import com.microsoft.azure.documentdb.Offer; +import com.microsoft.azure.documentdb.Permission; +import com.microsoft.azure.documentdb.RequestOptions; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.SqlQuerySpec; +import com.microsoft.azure.documentdb.StoredProcedure; +import com.microsoft.azure.documentdb.StoredProcedureResponse; +import com.microsoft.azure.documentdb.Trigger; +import com.microsoft.azure.documentdb.User; +import com.microsoft.azure.documentdb.UserDefinedFunction; +import com.microsoft.azure.documentdb.rx.internal.RxDocumentClientImpl; +import com.microsoft.azure.documentdb.rx.internal.RxWrapperDocumentClientImpl; + +import rx.Observable; + +/** + * Provides a client-side logical representation of the Azure DocumentDB + * database service. This async client is used to configure and execute requests + * against the service. + * + *

+ * {@link AsyncDocumentClient} async APIs return rxJava's {@code + * Observable}, and so you can use rxJava {@link Observable} functionalities. + * The async {@link Observable} based APIs perform the requested operation only after + * subscription. + * + *

+ * The service client encapsulates the endpoint and credentials used to access + * the DocumentDB service. + * + * To instantiate you can use the {@link Builder} + *

+ * {@code
+ *   AsyncDocumentClient client = new AsyncDocumentClient.Builder()
+ *           .withServiceEndpoint(serviceEndpoint)
+ *           .withMasterKey(masterKey)
+ *           .withConnectionPolicy(ConnectionPolicy.GetDefault())
+ *           .withConsistencyLevel(ConsistencyLevel.Session)
+ *           .build();
+ * }
+ * 
+ */ +public interface AsyncDocumentClient { + + /** + * Helper class to build {@link AsyncDocumentClient} instances + * as logical representation of the Azure DocumentDB database service. + * + *
+     * {@code
+     *   AsyncDocumentClient client = new AsyncDocumentClient.Builder()
+     *           .withServiceEndpoint(serviceEndpoint)
+     *           .withMasterKey(masterKey)
+     *           .withConnectionPolicy(ConnectionPolicy.GetDefault())
+     *           .withConsistencyLevel(ConsistencyLevel.Session)
+     *           .build();
+     * }
+     * 
+ */ + public static class Builder { + + private String masterKey; + private ConnectionPolicy connectionPolicy; + private ConsistencyLevel desiredConsistencyLevel; + private URI serviceEndpoint; + private int eventLoopSize = -1; + private int separateComputationPoolSize = -1; + + public Builder withServiceEndpoint(String serviceEndpoint) { + try { + this.serviceEndpoint = new URI(serviceEndpoint); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e.getMessage()); + } + return this; + } + + public Builder withMasterKey(String masterKey) { + this.masterKey = masterKey; + return this; + } + + public Builder withConsistencyLevel(ConsistencyLevel desiredConsistencyLevel) { + this.desiredConsistencyLevel = desiredConsistencyLevel; + return this; + } + + /** + * NOTE: This is experimental. + * If sets, modifies the event loop size and the computation pool size. + * + * As a rule of thumb (eventLoopSize + separateComputationPoolSize) ~ # + * CPU cores. If you have 8 CPU cores, eventLoopSize = 5, and + * separateComputationPoolSize = 3 is a logical choice for better throughput. + * + * Computation intensive work, e.g., authentication token generation, is + * performed on the computation scheduler. If + * separateComputationPoolSize is set to 0, computation will take place + * on the observable subscription thread. + * + * @param eventLoopSize the size of the event loop (the number of event loop threads). + * @param separateComputationPoolSize the size the thread pool backing computation scheduler up. + * @return Builder + */ + Builder withWorkers(int eventLoopSize, int separateComputationPoolSize) { + ifThrowIllegalArgException(eventLoopSize <= 0, "invalid event loop size"); + ifThrowIllegalArgException(separateComputationPoolSize < 0, "invalid computation scheduler pool size"); + this.eventLoopSize = eventLoopSize; + this.separateComputationPoolSize = separateComputationPoolSize; + return this; + } + + public Builder withConnectionPolicy(ConnectionPolicy connectionPolicy) { + this.connectionPolicy = connectionPolicy; + return this; + } + + private void ifThrowIllegalArgException(boolean value, String error) { + if (value) { + throw new IllegalArgumentException(error); + } + } + + public AsyncDocumentClient build() { + + ifThrowIllegalArgException(this.serviceEndpoint == null, "cannot build client without service endpoint"); + ifThrowIllegalArgException(this.masterKey == null, "cannot build client without masterKey"); + + if (connectionPolicy != null && ConnectionMode.DirectHttps != connectionPolicy.getConnectionMode()) { + return new RxDocumentClientImpl(serviceEndpoint, masterKey, connectionPolicy, desiredConsistencyLevel, + eventLoopSize, separateComputationPoolSize); + } else { + + ifThrowIllegalArgException(this.eventLoopSize != -1, "eventLoopSize is not applicable in direct mode"); + ifThrowIllegalArgException(this.separateComputationPoolSize != -1, "separateComputationPoolSize is not applicable in direct mode"); + + // fall back to RX wrapper with blocking IO + return new RxWrapperDocumentClientImpl( + new DocumentClient(serviceEndpoint.toString(), masterKey, connectionPolicy, desiredConsistencyLevel)); + } + } + } + + /** + * Gets the default service endpoint as passed in by the user during construction. + * + * @return the service endpoint URI + */ + URI getServiceEndpoint(); + + /** + * Gets the current write endpoint chosen based on availability and preference. + * + * @return the write endpoint URI + */ + URI getWriteEndpoint(); + + /** + * Gets the current read endpoint chosen based on availability and preference. + * + * @return the read endpoint URI + */ + URI getReadEndpoint(); + + /** + * Gets the connection policy + * + * @return the connection policy + */ + ConnectionPolicy getConnectionPolicy(); + + /** + * Creates a database. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created database. + * In case of failure the {@link Observable} will error. + * + * @param database the database. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the created database or an error. + */ + Observable> createDatabase(Database database, RequestOptions options); + + /** + * Deletes a database. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the deleted database. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the deleted database or an error. + */ + Observable> deleteDatabase(String databaseLink, RequestOptions options); + + /** + * Reads a database. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the read database. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the read database or an error. + */ + Observable> readDatabase(String databaseLink, RequestOptions options); + + /** + * Reads all databases. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the read databases. + * In case of failure the {@link Observable} will error. + * + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of read databases or an error. + */ + Observable> readDatabases(FeedOptions options); + + /** + * Query for databases. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the read databases. + * In case of failure the {@link Observable} will error. + * + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of read databases or an error. + */ + Observable> queryDatabases(String query, FeedOptions options); + + /** + * Query for databases. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the obtained databases. + * In case of failure the {@link Observable} will error. + * + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained databases or an error. + */ + Observable> queryDatabases(SqlQuerySpec querySpec, FeedOptions options); + + /** + * Creates a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created collection. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param collection the collection. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the created collection or an error. + */ + Observable> createCollection(String databaseLink, DocumentCollection collection, + RequestOptions options); + + /** + * Replaces a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced document collection. + * In case of failure the {@link Observable} will error. + * + * @param collection the document collection to use. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the replaced document collection or an error. + */ + Observable> replaceCollection(DocumentCollection collection, RequestOptions options); + + /** + * Deletes a document collection by the collection link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the deleted database. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the deleted database or an error. + + */ + Observable> deleteCollection(String collectionLink, RequestOptions options); + + /** + * Reads a document collection by the collection link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the read collection. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the read collection or an error. + */ + Observable> readCollection(String collectionLink, RequestOptions options); + + /** + * Reads all document collections in a database. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the read collections. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param options the fee options. + * @return the feed response with the read collections. + * @return an {@link Observable} containing one or several feed response pages of the read collections or an error. + */ + Observable> readCollections(String databaseLink, FeedOptions options); + + /** + * Query for document collections in a database. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the obtained collections. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained collections or an error. + */ + Observable> queryCollections(String databaseLink, String query, FeedOptions options); + + /** + * Query for document collections in a database. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the obtained collections. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained collections or an error. + */ + Observable> queryCollections(String databaseLink, SqlQuerySpec querySpec, FeedOptions options); + + /** + * Creates a document. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created document. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the link to the parent document collection. + * @param document the document represented as a POJO or Document object. + * @param options the request options. + * @param disableAutomaticIdGeneration the flag for disabling automatic id generation. + * @return an {@link Observable} containing the single resource response with the created document or an error. + + */ + Observable> createDocument(String collectionLink, Object document, RequestOptions options, + boolean disableAutomaticIdGeneration); + + /** + * Upserts a document. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the upserted document. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the link to the parent document collection. + * @param document the document represented as a POJO or Document object to upsert. + * @param options the request options. + * @param disableAutomaticIdGeneration the flag for disabling automatic id generation. + * @return an {@link Observable} containing the single resource response with the upserted document or an error. + */ + Observable> upsertDocument(String collectionLink, Object document, RequestOptions options, + boolean disableAutomaticIdGeneration); + + /** + * Replaces a document using a POJO object. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced document. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param document the document represented as a POJO or Document object. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the replaced document or an error. + */ + Observable> replaceDocument(String documentLink, Object document, RequestOptions options); + + /** + * Replaces a document with the passed in document. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced document. + * In case of failure the {@link Observable} will error. + * + * @param document the document to replace (containing the document id). + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the replaced document or an error. + */ + Observable> replaceDocument(Document document, RequestOptions options); + + /** + * Deletes a document by the document link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the deleted document. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the deleted document or an error. + */ + Observable> deleteDocument(String documentLink, RequestOptions options); + + /** + * Reads a document by the document link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the read document. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the read document or an error. + */ + Observable> readDocument(String documentLink, RequestOptions options); + + /** + * Reads all documents in a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the read documents. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the read documents or an error. + */ + Observable> readDocuments(String collectionLink, FeedOptions options); + + + /** + * Query for documents in a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the obtained documents. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the link to the parent document collection. + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained document or an error. + */ + Observable> queryDocuments(String collectionLink, String query, FeedOptions options); + + /** + * Query for documents in a document collection with a partitionKey + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the obtained documents. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the link to the parent document collection. + * @param query the query. + * @param options the feed options. + * @param partitionKey the partitionKey. + * @return an {@link Observable} containing one or several feed response pages of the obtained documents or an error. + */ + Observable> queryDocuments(String collectionLink, String query, FeedOptions options, + Object partitionKey); + + /** + * Query for documents in a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the obtained documents. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the link to the parent document collection. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained documents or an error. + */ + Observable> queryDocuments(String collectionLink, SqlQuerySpec querySpec, FeedOptions options); + + /** + * Query for documents in a document collection. + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response of the obtained documents. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the link to the parent document collection. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @param partitionKey the partitionKey. + * @return an {@link Observable} containing one or several feed response pages of the obtained documents or an error. + */ + Observable> queryDocuments(String collectionLink, SqlQuerySpec querySpec, FeedOptions options, + Object partitionKey); + + /** + * Creates a stored procedure. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created stored procedure. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param storedProcedure the stored procedure to create. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the created stored procedure or an error. + */ + Observable> createStoredProcedure(String collectionLink, StoredProcedure storedProcedure, + RequestOptions options); + + /** + * Upserts a stored procedure. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the upserted stored procedure. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param storedProcedure the stored procedure to upsert. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the upserted stored procedure or an error. + */ + Observable> upsertStoredProcedure(String collectionLink, StoredProcedure storedProcedure, + RequestOptions options); + + /** + * Replaces a stored procedure. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced stored procedure. + * In case of failure the {@link Observable} will error. + * + * @param storedProcedure the stored procedure to use. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the replaced stored procedure or an error. + */ + Observable> replaceStoredProcedure(StoredProcedure storedProcedure, RequestOptions options); + + /** + * Deletes a stored procedure by the stored procedure link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the deleted stored procedure. + * In case of failure the {@link Observable} will error. + * + * @param storedProcedureLink the stored procedure link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the deleted stored procedure or an error. + */ + Observable> deleteStoredProcedure(String storedProcedureLink, RequestOptions options); + + /** + * Read a stored procedure by the stored procedure link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the read stored procedure. + * In case of failure the {@link Observable} will error. + * + * @param storedProcedureLink the stored procedure link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the read stored procedure or an error. + */ + Observable> readStoredProcedure(String storedProcedureLink, RequestOptions options); + + /** + * Reads all stored procedures in a document collection link. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the read stored procedures. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the read stored procedures or an error. + */ + Observable> readStoredProcedures(String collectionLink, FeedOptions options); + + /** + * Query for stored procedures in a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained stored procedures. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained stored procedures or an error. + */ + Observable> queryStoredProcedures(String collectionLink, String query, FeedOptions options); + + /** + * Query for stored procedures in a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained stored procedures. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained stored procedures or an error. + */ + Observable> queryStoredProcedures(String collectionLink, SqlQuerySpec querySpec, + FeedOptions options); + + /** + * Executes a stored procedure by the stored procedure link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the stored procedure response. + * In case of failure the {@link Observable} will error. + * + * @param storedProcedureLink the stored procedure link. + * @param procedureParams the array of procedure parameter values. + * @return an {@link Observable} containing the single resource response with the stored procedure response or an error. + */ + Observable executeStoredProcedure(String storedProcedureLink, Object[] procedureParams); + + /** + * Executes a stored procedure by the stored procedure link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the stored procedure response. + * In case of failure the {@link Observable} will error. + * + * @param storedProcedureLink the stored procedure link. + * @param options the request options. + * @param procedureParams the array of procedure parameter values. + * @return an {@link Observable} containing the single resource response with the stored procedure response or an error. + */ + Observable executeStoredProcedure(String storedProcedureLink, RequestOptions options, + Object[] procedureParams); + + /** + * Creates a trigger. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created trigger. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param trigger the trigger. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the created trigger or an error. + */ + Observable> createTrigger(String collectionLink, Trigger trigger, RequestOptions options); + + /** + * Upserts a trigger. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the upserted trigger. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param trigger the trigger to upsert. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the upserted trigger or an error. + */ + Observable> upsertTrigger(String collectionLink, Trigger trigger, RequestOptions options); + + /** + * Replaces a trigger. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced trigger. + * In case of failure the {@link Observable} will error. + * + * @param trigger the trigger to use. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the replaced trigger or an error. + */ + Observable> replaceTrigger(Trigger trigger, RequestOptions options); + + /** + * Deletes a trigger. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the deleted trigger. + * In case of failure the {@link Observable} will error. + * + * @param triggerLink the trigger link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the deleted trigger or an error. + */ + Observable> deleteTrigger(String triggerLink, RequestOptions options); + + /** + * Reads a trigger by the trigger link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the read trigger. + * In case of failure the {@link Observable} will error. + * + * @param triggerLink the trigger link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the read trigger or an error. + */ + Observable> readTrigger(String triggerLink, RequestOptions options); + + /** + * Reads all triggers in a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the read triggers. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the read triggers or an error. + */ + Observable> readTriggers(String collectionLink, FeedOptions options); + + /** + * Query for triggers. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained triggers. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained triggers or an error. + */ + Observable> queryTriggers(String collectionLink, String query, FeedOptions options); + + /** + * Query for triggers. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained triggers. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained triggers or an error. + */ + Observable> queryTriggers(String collectionLink, SqlQuerySpec querySpec, FeedOptions options); + + /** + * Creates a user defined function. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created user defined function. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param udf the user defined function. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the created user defined function or an error. + */ + Observable> createUserDefinedFunction(String collectionLink, UserDefinedFunction udf, + RequestOptions options); + + /** + * Upserts a user defined function. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the upserted user defined function. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param udf the user defined function to upsert. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the upserted user defined function or an error. + */ + Observable> upsertUserDefinedFunction(String collectionLink, UserDefinedFunction udf, + RequestOptions options); + + /** + * Replaces a user defined function. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced user defined function. + * In case of failure the {@link Observable} will error. + * + * @param udf the user defined function. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the replaced user defined function or an error. + */ + Observable> replaceUserDefinedFunction(UserDefinedFunction udf, RequestOptions options); + + /** + * Deletes a user defined function. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the deleted user defined function. + * In case of failure the {@link Observable} will error. + * + * @param udfLink the user defined function link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the deleted user defined function or an error. + */ + Observable> deleteUserDefinedFunction(String udfLink, RequestOptions options); + + /** + * Read a user defined function. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the read user defined function. + * In case of failure the {@link Observable} will error. + * + * @param udfLink the user defined function link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the read user defined function or an error. + */ + Observable> readUserDefinedFunction(String udfLink, RequestOptions options); + + /** + * Reads all user defined functions in a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the read user defined functions. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the read user defined functions or an error. + */ + Observable> readUserDefinedFunctions(String collectionLink, FeedOptions options); + + /** + * Query for user defined functions. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained user defined functions. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained user defined functions or an error. + */ + Observable> queryUserDefinedFunctions(String collectionLink, String query, + FeedOptions options); + + /** + * Query for user defined functions. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained user defined functions. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained user defined functions or an error. + */ + Observable> queryUserDefinedFunctions(String collectionLink, SqlQuerySpec querySpec, + FeedOptions options); + + /** + * Creates an attachment. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created attachment. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param attachment the attachment to create. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the created attachment or an error. + */ + Observable> createAttachment(String documentLink, Attachment attachment, RequestOptions options); + + /** + * Upserts an attachment. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the upserted attachment. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param attachment the attachment to upsert. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the upserted attachment or an error. + */ + Observable> upsertAttachment(String documentLink, Attachment attachment, RequestOptions options); + + /** + * Replaces an attachment. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced attachment. + * In case of failure the {@link Observable} will error. + * + * @param attachment the attachment to use. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the replaced attachment or an error. + */ + Observable> replaceAttachment(Attachment attachment, RequestOptions options); + + /** + * Deletes an attachment. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the deleted attachment. + * In case of failure the {@link Observable} will error. + * + * @param attachmentLink the attachment link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the deleted attachment or an error. + */ + Observable> deleteAttachment(String attachmentLink, RequestOptions options); + + /** + * Reads an attachment. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the read attachment. + * In case of failure the {@link Observable} will error. + * + * @param attachmentLink the attachment link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the read attachment or an error. + */ + Observable> readAttachment(String attachmentLink, RequestOptions options); + + /** + * Reads all attachments in a document. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the read attachments. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the read attachments or an error. + */ + Observable> readAttachments(String documentLink, FeedOptions options); + + /** + * Query for attachments. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained attachments. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained attachments or an error. + */ + Observable> queryAttachments(String documentLink, String query, FeedOptions options); + + /** + * Query for attachments. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained attachments. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained attachments or an error. + */ + Observable> queryAttachments(String documentLink, SqlQuerySpec querySpec, FeedOptions options); + + /** + * Creates an attachment. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created attachment. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param mediaStream the media stream for creating the attachment. + * @param options the media options. + * @return an {@link Observable} containing the single resource response with the created attachment or an error. + */ + Observable> createAttachment(String documentLink, InputStream mediaStream, MediaOptions options); + + /** + * Upserts an attachment to the media stream + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the upserted attachment. + * In case of failure the {@link Observable} will error. + * + * @param documentLink the document link. + * @param mediaStream the media stream for upserting the attachment. + * @param options the media options. + * @return an {@link Observable} containing the single resource response with the upserted attachment or an error. + */ + Observable> upsertAttachment(String documentLink, InputStream mediaStream, MediaOptions options); + + /** + * Reads a media by the media link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single media response. + * In case of failure the {@link Observable} will error. + * + * @param mediaLink the media link. + * @return an {@link Observable} containing the single meadia response or an error. + */ + Observable readMedia(String mediaLink); + + /** + * Updates a media by the media link. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single media response. + * In case of failure the {@link Observable} will error. + * + * @param mediaLink the media link. + * @param mediaStream the media stream to upload. + * @param options the media options. + * @return an {@link Observable} containing the single meadia response or an error. + */ + Observable updateMedia(String mediaLink, InputStream mediaStream, MediaOptions options); + + /** + * Reads a conflict. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the read conflict. + * In case of failure the {@link Observable} will error. + * + * @param conflictLink the conflict link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the read conflict or an error. + */ + Observable> readConflict(String conflictLink, RequestOptions options); + + /** + * Reads all conflicts in a document collection. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the read conflicts. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the read conflicts or an error. + */ + Observable> readConflicts(String collectionLink, FeedOptions options); + + /** + * Query for conflicts. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained conflicts. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained conflicts or an error. + */ + Observable> queryConflicts(String collectionLink, String query, FeedOptions options); + + /** + * Query for conflicts. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained conflicts. + * In case of failure the {@link Observable} will error. + * + * @param collectionLink the collection link. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained conflicts or an error. + */ + Observable> queryConflicts(String collectionLink, SqlQuerySpec querySpec, FeedOptions options); + + /** + * Deletes a conflict. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the deleted conflict. + * In case of failure the {@link Observable} will error. + * + * @param conflictLink the conflict link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the deleted conflict or an error. + */ + Observable> deleteConflict(String conflictLink, RequestOptions options); + + /** + * Creates a user. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created user. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param user the user to create. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the created user or an error. + */ + Observable> createUser(String databaseLink, User user, RequestOptions options); + + /** + * Upserts a user. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the upserted user. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param user the user to upsert. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the upserted user or an error. + */ + Observable> upsertUser(String databaseLink, User user, RequestOptions options); + + /** + * Replaces a user. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced user. + * In case of failure the {@link Observable} will error. + * + * @param user the user to use. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the replaced user or an error. + */ + Observable> replaceUser(User user, RequestOptions options); + + /** + * Deletes a user. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the deleted user. + * In case of failure the {@link Observable} will error. + * + * @param userLink the user link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the deleted user or an error. + */ + Observable> deleteUser(String userLink, RequestOptions options); + + /** + * Reads a user. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the read user. + * In case of failure the {@link Observable} will error. + * + * @param userLink the user link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the read user or an error. + */ + Observable> readUser(String userLink, RequestOptions options); + + /** + * Reads all users in a database. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the read users. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the read users or an error. + */ + Observable> readUsers(String databaseLink, FeedOptions options); + + /** + * Query for users. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained users. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained users or an error. + */ + Observable> queryUsers(String databaseLink, String query, FeedOptions options); + + /** + * Query for users. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained users. + * In case of failure the {@link Observable} will error. + * + * @param databaseLink the database link. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained users or an error. + */ + Observable> queryUsers(String databaseLink, SqlQuerySpec querySpec, FeedOptions options); + + /** + * Creates a permission. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the created permission. + * In case of failure the {@link Observable} will error. + * + * @param userLink the user link. + * @param permission the permission to create. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the created permission or an error. + */ + Observable> createPermission(String userLink, Permission permission, RequestOptions options); + + /** + * Upserts a permission. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the upserted permission. + * In case of failure the {@link Observable} will error. + * + * @param userLink the user link. + * @param permission the permission to upsert. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the upserted permission or an error. + */ + Observable> upsertPermission(String userLink, Permission permission, RequestOptions options); + + /** + * Replaces a permission. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced permission. + * In case of failure the {@link Observable} will error. + * + * @param permission the permission to use. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the replaced permission or an error. + */ + Observable> replacePermission(Permission permission, RequestOptions options); + + /** + * Deletes a permission. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response for the deleted permission. + * In case of failure the {@link Observable} will error. + * + * @param permissionLink the permission link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response for the deleted permission or an error. + */ + Observable> deletePermission(String permissionLink, RequestOptions options); + + /** + * Reads a permission. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the read permission. + * In case of failure the {@link Observable} will error. + * + * @param permissionLink the permission link. + * @param options the request options. + * @return an {@link Observable} containing the single resource response with the read permission or an error. + */ + Observable> readPermission(String permissionLink, RequestOptions options); + + /** + * Reads all permissions. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the read permissions. + * In case of failure the {@link Observable} will error. + * + * @param permissionLink the permission link. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the read permissions or an error. + */ + Observable> readPermissions(String permissionLink, FeedOptions options); + + /** + * Query for permissions. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained permissions. + * In case of failure the {@link Observable} will error. + * + * @param permissionLink the permission link. + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained permissions or an error. + */ + Observable> queryPermissions(String permissionLink, String query, FeedOptions options); + + /** + * Query for permissions. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the obtained permissions. + * In case of failure the {@link Observable} will error. + * + * @param permissionLink the permission link. + * @param querySpec the SQL query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained permissions or an error. + */ + Observable> queryPermissions(String permissionLink, SqlQuerySpec querySpec, FeedOptions options); + + /** + * Replaces an offer. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the replaced offer. + * In case of failure the {@link Observable} will error. + * + * @param offer the offer to use. + * @return an {@link Observable} containing the single resource response with the replaced offer or an error. + */ + Observable> replaceOffer(Offer offer); + + /** + * Reads an offer. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the read offer. + * In case of failure the {@link Observable} will error. + * + * @param offerLink the offer link. + * @return an {@link Observable} containing the single resource response with the read offer or an error. + */ + Observable> readOffer(String offerLink); + + /** + * Reads offers. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of the read offers. + * In case of failure the {@link Observable} will error. + * + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the read offers or an error. + */ + Observable> readOffers(FeedOptions options); + + /** + * Query for offers in a database. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of obtained obtained offers. + * In case of failure the {@link Observable} will error. + * + * @param query the query. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained offers or an error. + */ + Observable> queryOffers(String query, FeedOptions options); + + /** + * Query for offers in a database. + * + * After subscription the operation will be performed. + * The {@link Observable} will contain one or several feed response pages of obtained obtained offers. + * In case of failure the {@link Observable} will error. + * + * @param querySpec the query specification. + * @param options the feed options. + * @return an {@link Observable} containing one or several feed response pages of the obtained offers or an error. + */ + Observable> queryOffers(SqlQuerySpec querySpec, FeedOptions options); + + /** + * Gets database account information. + * + * After subscription the operation will be performed. + * The {@link Observable} upon successful completion will contain a single resource response with the database account. + * In case of failure the {@link Observable} will error. + * + * @return an {@link Observable} containing the single resource response with the database account or an error. + */ + Observable getDatabaseAccount(); + + /** + * Close this {@link AsyncDocumentClient} instance and cleans up the resources. + */ + void close(); + +} \ No newline at end of file diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/Constants.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/Constants.java new file mode 100644 index 000000000000..c07f2eadf13a --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/Constants.java @@ -0,0 +1,31 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + +public class Constants { + + public static class Versions { + public static final String SDK_VERSION = "0.9.0-SNAPSHOT"; + public static final String SDK_NAME = "documentdb-rxjava-sdk"; + } +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/CreateDocumentRetryHandler.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/CreateDocumentRetryHandler.java new file mode 100644 index 000000000000..f19b7eaf69e8 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/CreateDocumentRetryHandler.java @@ -0,0 +1,83 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.internal.HttpConstants; +import com.microsoft.azure.documentdb.internal.RetryPolicy; +import com.microsoft.azure.documentdb.internal.RetryPolicyBridgeInternal; +import com.microsoft.azure.documentdb.internal.routing.ClientCollectionCache; + +import rx.Observable; + +class CreateDocumentRetryHandler implements RxRetryHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(CreateDocumentRetryHandler.class); + private final RetryPolicy keyMismatchRetryPolicy; + + public CreateDocumentRetryHandler(ClientCollectionCache clientCollectionCache, + String resourcePath) { + + this.keyMismatchRetryPolicy = RetryPolicyBridgeInternal + .createPartitionKeyMismatchRetryPolicy(resourcePath, clientCollectionCache); + } + + @Override + public Observable handleRetryAttempt(Throwable t, int attemptNumber) { + + if (t instanceof DocumentClientException) { + try { + return handleRetryAttemptInternal((DocumentClientException) t, attemptNumber); + } catch (DocumentClientException e) { + return Observable.error(e); + } + } else { + return Observable.error(t); + } + } + + private Observable handleRetryAttemptInternal(DocumentClientException e, int attemptNumber) throws DocumentClientException { + + RetryPolicy retryPolicy = null; + if (e.getStatusCode() == HttpConstants.StatusCodes.BADREQUEST && e.getSubStatusCode() != null + && e.getSubStatusCode() == HttpConstants.SubStatusCodes.PARTITION_KEY_MISMATCH) { + // If HttpStatusCode is 404 (NotFound) and SubStatusCode is + // 1001 (PartitionKeyMismatch), invoke the partition key mismatch retry policy + retryPolicy = keyMismatchRetryPolicy; + } + + if (retryPolicy == null || !retryPolicy.shouldRetry(e)) { + LOGGER.trace("Execution encontured exception: {}, status code {} sub status code {}. Won't retry!", + e.getMessage(), e.getStatusCode(), e.getSubStatusCode()); + return Observable.error(e); + } + LOGGER.trace("Execution encontured exception: {}, status code {} sub status code {}. Will retry in {}ms", + e.getMessage(), e.getStatusCode(), e.getSubStatusCode(), retryPolicy.getRetryAfterInMilliseconds()); + + long waitTime = retryPolicy.getRetryAfterInMilliseconds(); + return Observable.just(waitTime); + } +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/ExecuteDocumentClientRequestRetryHandler.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/ExecuteDocumentClientRequestRetryHandler.java new file mode 100644 index 000000000000..112d892a3e65 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/ExecuteDocumentClientRequestRetryHandler.java @@ -0,0 +1,110 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.internal.RetryPolicyBridgeInternal; +import com.microsoft.azure.documentdb.internal.EndpointManager; +import com.microsoft.azure.documentdb.internal.HttpConstants; +import com.microsoft.azure.documentdb.internal.RetryPolicy; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; + +import rx.Observable; + +/** + * Provides a Retry handler for executing the code block and retry if needed. + */ +class ExecuteDocumentClientRequestRetryHandler implements RxRetryHandler { + private final static Logger LOGGER = LoggerFactory.getLogger(ExecuteDocumentClientRequestRetryHandler.class); + + private final RetryPolicy discoveryRetryPolicy; + private final RetryPolicy throttleRetryPolicy; + private final RetryPolicy sessionReadRetryPolicy; + + public ExecuteDocumentClientRequestRetryHandler(RxDocumentServiceRequest request, + EndpointManager globalEndpointManager, AsyncDocumentClient client) { + + this.discoveryRetryPolicy = RetryPolicyBridgeInternal.createEndpointDiscoveryRetryPolicy( + client.getConnectionPolicy(), + globalEndpointManager); + + this.throttleRetryPolicy = RetryPolicyBridgeInternal.createResourceThrottleRetryPolicy( + client.getConnectionPolicy().getRetryOptions().getMaxRetryAttemptsOnThrottledRequests(), + client.getConnectionPolicy().getRetryOptions().getMaxRetryWaitTimeInSeconds()); + + this.sessionReadRetryPolicy = RetryPolicyBridgeInternal.createSessionReadRetryPolicy( + globalEndpointManager, request); + } + + @Override + public Observable handleRetryAttempt(Throwable t, int attemptNumber) { + + if (t instanceof DocumentClientException) { + try { + return handleRetryAttemptInternal((DocumentClientException) t, attemptNumber); + } catch (Exception e) { + return Observable.error(e); + } + } else { + return Observable.error(t); + } + } + + public Observable handleRetryAttemptInternal(DocumentClientException e, int attemptNumber) throws DocumentClientException { + + LOGGER.trace("Executing DocumentClientRequest"); + + RetryPolicy retryPolicy = null; + if (e.getStatusCode() == HttpConstants.StatusCodes.FORBIDDEN && e.getSubStatusCode() != null + && e.getSubStatusCode() == HttpConstants.SubStatusCodes.FORBIDDEN_WRITEFORBIDDEN) { + // If HttpStatusCode is 403 (Forbidden) and SubStatusCode is + // 3 (WriteForbidden), + // invoke the endpoint discovery retry policy + retryPolicy = discoveryRetryPolicy; + } else if (e.getStatusCode() == HttpConstants.StatusCodes.TOO_MANY_REQUESTS) { + // If HttpStatusCode is 429 (Too Many Requests), invoke the + // throttle retry policy + retryPolicy = throttleRetryPolicy; + } else if (e.getStatusCode() == HttpConstants.StatusCodes.NOTFOUND && e.getSubStatusCode() != null + && e.getSubStatusCode() == HttpConstants.SubStatusCodes.READ_SESSION_NOT_AVAILABLE) { + // If HttpStatusCode is 404 (NotFound) and SubStatusCode is + // 1002 (ReadSessionNotAvailable), invoke the session read retry policy + retryPolicy = sessionReadRetryPolicy; + } + + if (retryPolicy == null || !retryPolicy.shouldRetry(e)) { + LOGGER.trace("Execution encontured exception: {}, status code {} sub status code {}. Won't retry!", + e.getMessage(), e.getStatusCode(), e.getSubStatusCode()); + return Observable.error(e); + } + LOGGER.trace("Execution encontured exception: {}, status code {} sub status code {}. Will retry in {}ms", + e.getMessage(), e.getStatusCode(), e.getSubStatusCode(), retryPolicy.getRetryAfterInMilliseconds()); + + return Observable.timer(retryPolicy.getRetryAfterInMilliseconds(), TimeUnit.MILLISECONDS); + } +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RetryFunctionFactory.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RetryFunctionFactory.java new file mode 100644 index 000000000000..7acf997d9871 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RetryFunctionFactory.java @@ -0,0 +1,88 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.microsoft.azure.documentdb.DocumentClientException; + +import rx.Observable; +import rx.functions.Func1; + +class RetryFunctionFactory { + + private final static Logger LOGGER = LoggerFactory.getLogger(RetryFunctionFactory.class); + + // this is just a safe guard, to ensure even if the retry policy doesn't give up we avoid infinite retries. + private final static int MAX_RETRIES_LIMIT = 200; + + public static Func1, Observable> from(RxRetryHandler retryPolicy) { + return new Func1, Observable>() { + + @Override + public Observable call(final Observable failures) { + + return failures + .zipWith(Observable.range(1, MAX_RETRIES_LIMIT), + (err, attempt) -> + attempt < MAX_RETRIES_LIMIT ? + handleRetryAttempt(err, attempt, retryPolicy) : + Observable.error(extractDocumentClientCause(err, attempt)) ) + .flatMap(x -> x); + } + }; + } + + private static Throwable extractDocumentClientCause(Throwable t, int attemptNumber) { + if (t instanceof DocumentClientException) { + return t; + } else if (t instanceof RuntimeException && t.getCause() instanceof DocumentClientException) { + return t.getCause(); + } else { + LOGGER.warn("unknown failure, cannot retry [{}], attempt number [{}]", t.getMessage(), attemptNumber, t); + return t; + } + } + + private static Observable handleRetryAttempt(Throwable t, int attemptNumber, RxRetryHandler retryPolicy) { + Throwable cause = extractDocumentClientCause(t, attemptNumber); + + if (LOGGER.isDebugEnabled()) { + if (cause instanceof DocumentClientException) { + DocumentClientException ex = (DocumentClientException) cause; + LOGGER.debug("Handling Failure Attempt [{}], StatusCode [{}], SubStatusCode," + + " Error: [{}] ", attemptNumber, ex.getStatusCode(), ex.getSubStatusCode(), ex.getError(), ex); + } else { + LOGGER.debug("Handling Failure Attempt [{}], req [{}]", attemptNumber, cause); + } + } + + try { + return retryPolicy.handleRetryAttempt(cause, attemptNumber); + } catch (Exception e) { + return Observable.error(e); + } + } +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxDocumentClientImpl.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxDocumentClientImpl.java new file mode 100644 index 000000000000..900e34880953 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxDocumentClientImpl.java @@ -0,0 +1,1638 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + +import static com.microsoft.azure.documentdb.BridgeInternal.documentFromObject; +import static com.microsoft.azure.documentdb.BridgeInternal.toResourceResponse; + +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.microsoft.azure.documentdb.Attachment; +import com.microsoft.azure.documentdb.BridgeInternal; +import com.microsoft.azure.documentdb.Conflict; +import com.microsoft.azure.documentdb.ConnectionMode; +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.DatabaseAccount; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClient; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.FeedOptions; +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.MediaOptions; +import com.microsoft.azure.documentdb.MediaResponse; +import com.microsoft.azure.documentdb.Offer; +import com.microsoft.azure.documentdb.Permission; +import com.microsoft.azure.documentdb.RequestOptions; +import com.microsoft.azure.documentdb.Resource; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.SqlQuerySpec; +import com.microsoft.azure.documentdb.StoredProcedure; +import com.microsoft.azure.documentdb.StoredProcedureResponse; +import com.microsoft.azure.documentdb.Trigger; +import com.microsoft.azure.documentdb.User; +import com.microsoft.azure.documentdb.UserDefinedFunction; +import com.microsoft.azure.documentdb.internal.BaseAuthorizationTokenProvider; +import com.microsoft.azure.documentdb.internal.DocumentServiceResponse; +import com.microsoft.azure.documentdb.internal.EndpointManager; +import com.microsoft.azure.documentdb.internal.HttpConstants; +import com.microsoft.azure.documentdb.internal.OperationType; +import com.microsoft.azure.documentdb.internal.Paths; +import com.microsoft.azure.documentdb.internal.QueryCompatibilityMode; +import com.microsoft.azure.documentdb.internal.ResourceType; +import com.microsoft.azure.documentdb.internal.RuntimeConstants; +import com.microsoft.azure.documentdb.internal.SessionContainer; +import com.microsoft.azure.documentdb.internal.UserAgentContainer; +import com.microsoft.azure.documentdb.internal.Utils; +import com.microsoft.azure.documentdb.internal.routing.ClientCollectionCache; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; + +import io.netty.buffer.ByteBuf; +import io.reactivex.netty.RxNetty; +import io.reactivex.netty.channel.RxEventLoopProvider; +import io.reactivex.netty.channel.SingleNioLoopProvider; +import io.reactivex.netty.client.RxClient.ClientConfig; +import io.reactivex.netty.pipeline.ssl.DefaultFactories; +import io.reactivex.netty.protocol.http.client.HttpClient; +import io.reactivex.netty.protocol.http.client.HttpClientBuilder; +import rx.Observable; +import rx.Scheduler; +import rx.functions.Func1; +import rx.internal.util.RxThreadFactory; +import rx.schedulers.Schedulers; + +public class RxDocumentClientImpl implements AsyncDocumentClient { + + private final static int MAX_COLLECTION_CACHE_CONCURRENCY = 10; + private final Logger logger = LoggerFactory.getLogger(RxDocumentClientImpl.class); + private final String masterKey; + private final ExecutorService collectionCacheExecutorService; + private final URI serviceEndpoint; + private final ConnectionPolicy connectionPolicy; + private final SessionContainer sessionContainer; + private final ConsistencyLevel consistencyLevel; + private final BaseAuthorizationTokenProvider authorizationTokenProvider; + private final ClientCollectionCache collectionCache; + private final RxGatewayStoreModel gatewayProxy; + private final RxWrapperDocumentClientImpl rxWrapperClient; + private final Scheduler computationScheduler; + private Map resourceTokens; + /** + * Compatibility mode: Allows to specify compatibility mode used by client + * when making query requests. Should be removed when application/sql is no + * longer supported. + */ + private final QueryCompatibilityMode queryCompatibilityMode = QueryCompatibilityMode.Default; + private final HttpClient rxClient; + private final EndpointManager globalEndpointManager; + private final ExecutorService computationExecutor; + + public RxDocumentClientImpl(URI serviceEndpoint, String masterKey, ConnectionPolicy connectionPolicy, + ConsistencyLevel consistencyLevel, int eventLoopSize, int computationPoolSize) { + + logger.info("Initializing DocumentClient with" + + " serviceEndpoint [{}], ConnectionPolicy [{}], ConsistencyLevel [{}]", + serviceEndpoint, connectionPolicy, consistencyLevel); + + this.masterKey = masterKey; + this.serviceEndpoint = serviceEndpoint; + + if (connectionPolicy != null) { + this.connectionPolicy = connectionPolicy; + } else { + this.connectionPolicy = new ConnectionPolicy(); + } + + this.sessionContainer = new SessionContainer(this.serviceEndpoint.getHost()); + this.consistencyLevel = consistencyLevel; + + UserAgentContainer userAgentContainer = new UserAgentContainer(Constants.Versions.SDK_NAME, Constants.Versions.SDK_VERSION); + String userAgentSuffix = this.connectionPolicy.getUserAgentSuffix(); + if (userAgentSuffix != null && userAgentSuffix.length() > 0) { + userAgentContainer.setSuffix(userAgentSuffix); + } + + if (eventLoopSize <= 0) { + int cpuCount = Runtime.getRuntime().availableProcessors(); + if (cpuCount >= 4) { + // do authentication token generation on a scheduler + computationPoolSize = (cpuCount / 4); + eventLoopSize = cpuCount - computationPoolSize; + } else { + // do authentication token generation on subscription thread + computationPoolSize = 0; + eventLoopSize = cpuCount; + } + logger.debug("Auto configuring eventLoop size and computation pool size. CPU cores {[]}, eventLoopSize [{}], computationPoolSize [{}]", + cpuCount, eventLoopSize, computationPoolSize); + } + + logger.debug("EventLoop size [{}]", eventLoopSize); + + synchronized (RxDocumentClientImpl.class) { + SingleNioLoopProvider rxEventLoopProvider = new SingleNioLoopProvider(1, eventLoopSize); + RxEventLoopProvider oldEventLoopProvider = RxNetty.useEventLoopProvider(rxEventLoopProvider); + this.rxClient = httpClientBuilder().build(); + RxNetty.useEventLoopProvider(oldEventLoopProvider); + } + + if (computationPoolSize > 0) { + logger.debug("Intensive computation configured on a computation scheduler backed by thread pool size [{}]", computationPoolSize); + this.computationExecutor = new ThreadPoolExecutor(computationPoolSize, computationPoolSize, + 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue(2), + new RxThreadFactory("rxdocdb-computation"), new CallerRunsPolicy()); + + this.computationScheduler = Schedulers.from(this.computationExecutor); + } else { + logger.debug("Intensive computation configured on the subscription thread"); + this.computationExecutor = null; + this.computationScheduler = Schedulers.immediate(); + } + + this.authorizationTokenProvider = new BaseAuthorizationTokenProvider(this.masterKey); + this.collectionCacheExecutorService = new ThreadPoolExecutor(1, MAX_COLLECTION_CACHE_CONCURRENCY, 10, + TimeUnit.MINUTES, new ArrayBlockingQueue(MAX_COLLECTION_CACHE_CONCURRENCY, true), + new ThreadPoolExecutor.CallerRunsPolicy()); + + this.collectionCache = BridgeInternal.createClientCollectionCache(this, collectionCacheExecutorService); + + this.globalEndpointManager = BridgeInternal.createGlobalEndpointManager(this); + + this.gatewayProxy = new RxGatewayStoreModel(this.connectionPolicy, consistencyLevel, this.queryCompatibilityMode, + this.masterKey, this.resourceTokens, userAgentContainer, this.globalEndpointManager, this.rxClient); + + this.rxWrapperClient = new RxWrapperDocumentClientImpl( + new DocumentClient(serviceEndpoint.toString(), masterKey, connectionPolicy, consistencyLevel)); + + // If DirectHttps mode is configured in AsyncDocumentClient.Builder we fallback + // to RxWrapperDocumentClientImpl. So we should never get here + + if (this.connectionPolicy.getConnectionMode() == ConnectionMode.DirectHttps) { + throw new UnsupportedOperationException("Direct Https is not supported"); + } + } + + private HttpClientBuilder httpClientBuilder() { + HttpClientBuilder builder = RxNetty + .newHttpClientBuilder(this.serviceEndpoint.getHost(), this.serviceEndpoint.getPort()) + .withSslEngineFactory(DefaultFactories.trustAll()).withMaxConnections(connectionPolicy.getMaxPoolSize()) + .withIdleConnectionsTimeoutMillis(this.connectionPolicy.getIdleConnectionTimeout() * 1000); + + ClientConfig config = new ClientConfig.Builder() + .readTimeout(connectionPolicy.getRequestTimeout(), TimeUnit.SECONDS).build(); + return builder.config(config); + } + + @Override + public URI getServiceEndpoint() { + return this.serviceEndpoint; + } + + @Override + public URI getWriteEndpoint() { + return this.globalEndpointManager.getWriteEndpoint(); + } + + @Override + public URI getReadEndpoint() { + return this.globalEndpointManager.getReadEndpoint(); + } + + @Override + public ConnectionPolicy getConnectionPolicy() { + return this.connectionPolicy; + } + + @Override + public Observable> createDatabase(Database database, RequestOptions options) { + + return Observable.defer(() -> { + try { + + if (database == null) { + throw new IllegalArgumentException("Database"); + } + + logger.debug("Creating a Database. id: [{}]", database.getId()); + validateResource(database); + + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Create, + ResourceType.Database, Paths.DATABASES_ROOT, database, requestHeaders); + + return this.doCreate(request).map(response -> toResourceResponse(response, Database.class)); + } catch (Exception e) { + logger.debug("Failure in creating a database. due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> deleteDatabase(String databaseLink, RequestOptions options) { + return Observable.defer(() -> { + try { + if (StringUtils.isEmpty(databaseLink)) { + throw new IllegalArgumentException("databaseLink"); + } + + logger.debug("Deleting a Database. databaseLink: [{}]", databaseLink); + String path = Utils.joinPath(databaseLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Delete, + ResourceType.Database, path, requestHeaders); + + return this.doDelete(request).map(response -> toResourceResponse(response, Database.class)); + } catch (Exception e) { + logger.debug("Failure in deleting a database. due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> readDatabase(String databaseLink, RequestOptions options) { + + return Observable.defer(() -> { + try { + if (StringUtils.isEmpty(databaseLink)) { + throw new IllegalArgumentException("databaseLink"); + } + + logger.debug("Reading a Database. databaseLink: [{}]", databaseLink); + String path = Utils.joinPath(databaseLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Read, + ResourceType.Database, path, requestHeaders); + + return this.doRead(request).map(response -> toResourceResponse(response, Database.class)); + } catch (Exception e) { + logger.debug("Failure in reading a database. due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> readDatabases(FeedOptions options) { + return this.rxWrapperClient.readDatabases(options); + } + + @Override + public Observable> queryDatabases(String query, FeedOptions options) { + return this.rxWrapperClient.queryDatabases(query, options); + } + + @Override + public Observable> queryDatabases(SqlQuerySpec querySpec, FeedOptions options) { + return this.rxWrapperClient.queryDatabases(querySpec, options); + } + + @Override + public Observable> createCollection(String databaseLink, + DocumentCollection collection, RequestOptions options) { + + return Observable.defer(() -> { + try { + if (StringUtils.isEmpty(databaseLink)) { + throw new IllegalArgumentException("databaseLink"); + } + if (collection == null) { + throw new IllegalArgumentException("collection"); + } + + logger.debug("Creating a Collection. databaseLink: [{}], Collection id: [{}]", databaseLink, + collection.getId()); + validateResource(collection); + + String path = Utils.joinPath(databaseLink, Paths.COLLECTIONS_PATH_SEGMENT); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Create, + ResourceType.DocumentCollection, path, collection, requestHeaders); + return this.doCreate(request).map(response -> toResourceResponse(response, DocumentCollection.class)); + } catch (Exception e) { + logger.debug("Failure in creating a collection. due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> replaceCollection(DocumentCollection collection, + RequestOptions options) { + return Observable.defer(() -> { + try { + if (collection == null) { + throw new IllegalArgumentException("collection"); + } + + logger.debug("Replacing a Collection. id: [{}]", collection.getId()); + validateResource(collection); + + String path = Utils.joinPath(collection.getSelfLink(), null); + Map requestHeaders = this.getRequestHeaders(options); + + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Replace, + ResourceType.DocumentCollection, path, collection, requestHeaders); + + return this.doReplace(request).map(response -> toResourceResponse(response, DocumentCollection.class)); + + } catch (Exception e) { + logger.debug("Failure in replacing a collection. due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> deleteCollection(String collectionLink, + RequestOptions options) { + return Observable.defer(() -> { + try { + if (StringUtils.isEmpty(collectionLink)) { + throw new IllegalArgumentException("collectionLink"); + } + + logger.debug("Deleting a Collection. collectionLink: [{}]", collectionLink); + String path = Utils.joinPath(collectionLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Delete, + ResourceType.DocumentCollection, path, requestHeaders); + return this.doDelete(request).map(response -> toResourceResponse(response, DocumentCollection.class)); + + } catch (Exception e) { + logger.debug("Failure in deleting a collection, due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + private Observable doDelete(RxDocumentServiceRequest request) + throws DocumentClientException { + + Observable responseObservable = Observable.defer(() -> { + try { + return this.gatewayProxy.doDelete(request).doOnNext(response -> { + if (request.getResourceType() != ResourceType.DocumentCollection) { + this.captureSessionToken(request, response); + } else { + this.clearToken(request, response); + } + }); + } catch (Exception e) { + return Observable.error(e); + } + }).retryWhen(createExecuteRequestRetryHandler(request)); + + return createPutMoreContentObservable(request, HttpConstants.HttpMethods.DELETE) + .doOnNext(req -> this.applySessionToken(request)) + .flatMap(req -> responseObservable); + } + + private Observable doRead(RxDocumentServiceRequest request) + throws DocumentClientException { + + Observable responseObservable = Observable.defer(() -> { + try { + return this.gatewayProxy.processMessage(request).doOnNext(response -> { + this.captureSessionToken(request, response); + }); + } catch (Exception e) { + return Observable.error(e); + } + }).retryWhen(createExecuteRequestRetryHandler(request)); + + return createPutMoreContentObservable(request, HttpConstants.HttpMethods.GET) + .doOnNext(req -> this.applySessionToken(request)) + .flatMap(req -> responseObservable); + } + + @Override + public Observable> readCollection(String collectionLink, + RequestOptions options) { + + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + if (StringUtils.isEmpty(collectionLink)) { + throw new IllegalArgumentException("collectionLink"); + } + + logger.debug("Reading a Collection. collectionLink: [{}]", collectionLink); + String path = Utils.joinPath(collectionLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Read, + ResourceType.DocumentCollection, path, requestHeaders); + + return this.doRead(request).map(response -> toResourceResponse(response, DocumentCollection.class)); + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in reading a collection, due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> readCollections(String databaseLink, FeedOptions options) { + return this.rxWrapperClient.readCollections(databaseLink, options); + } + + @Override + public Observable> queryCollections(String databaseLink, String query, + FeedOptions options) { + return this.rxWrapperClient.queryCollections(databaseLink, query, options); + } + + @Override + public Observable> queryCollections(String databaseLink, + SqlQuerySpec querySpec, FeedOptions options) { + return this.rxWrapperClient.queryCollections(databaseLink, querySpec, options); + } + + private String getTargetDocumentCollectionLink(String collectionLink, Object document) { + if (StringUtils.isEmpty(collectionLink)) { + throw new IllegalArgumentException("collectionLink"); + } + if (document == null) { + throw new IllegalArgumentException("document"); + } + + String documentCollectionLink = collectionLink; + if (Utils.isDatabaseLink(collectionLink)) { + + // TODO: not supported yet + + // // Gets the partition resolver(if it exists) for the specified + // database link + // PartitionResolver partitionResolver = + // this.getPartitionResolver(collectionLink); + // + // // If the partition resolver exists, get the collection to which + // the Create/Upsert should be directed using the partition key + // if (partitionResolver != null) { + // documentCollectionLink = + // partitionResolver.resolveForCreate(document); + // } else { + // throw new + // IllegalArgumentException(PartitionResolverErrorMessage); + // } + } + + return documentCollectionLink; + } + + private static void validateResource(Resource resource) { + BridgeInternal.validateResource(resource); + } + + private Map getRequestHeaders(RequestOptions options) { + return BridgeInternal.getRequestHeaders(options); + } + + private void addPartitionKeyInformation(RxDocumentServiceRequest request, Document document, RequestOptions options, + DocumentCollection collection) { + BridgeInternal.addPartitionKeyInformation(request, document, options, collection); + } + + private RxDocumentServiceRequest getCreateDocumentRequest(String documentCollectionLink, Object document, + RequestOptions options, boolean disableAutomaticIdGeneration, OperationType operationType) { + + if (StringUtils.isEmpty(documentCollectionLink)) { + throw new IllegalArgumentException("documentCollectionLink"); + } + if (document == null) { + throw new IllegalArgumentException("document"); + } + + Document typedDocument = documentFromObject(document); + + RxDocumentClientImpl.validateResource(typedDocument); + + if (typedDocument.getId() == null && !disableAutomaticIdGeneration) { + // We are supposed to use GUID. Basically UUID is the same as GUID + // when represented as a string. + typedDocument.setId(UUID.randomUUID().toString()); + } + String path = Utils.joinPath(documentCollectionLink, Paths.DOCUMENTS_PATH_SEGMENT); + Map requestHeaders = this.getRequestHeaders(options); + + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(operationType, ResourceType.Document, path, + typedDocument, requestHeaders); + + // NOTE: if the collection is not currently cached this will be a + // blocking call + DocumentCollection collection = this.collectionCache.resolveCollection(request); + + this.addPartitionKeyInformation(request, typedDocument, options, collection); + return request; + } + + private void putMoreContentIntoDocumentServiceRequest(RxDocumentServiceRequest request, String httpMethod) { + if (this.masterKey != null) { + final Date currentTime = new Date(); + final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + sdf.setTimeZone(TimeZone.getTimeZone("GMT")); + String xDate = sdf.format(currentTime); + + request.getHeaders().put(HttpConstants.HttpHeaders.X_DATE, xDate); + } + + if (this.masterKey != null || this.resourceTokens != null) { + String resourceName = request.getResourceFullName(); + String authorization = this.getAuthorizationToken(resourceName, request.getPath(), + request.getResourceType(), httpMethod, request.getHeaders(), this.masterKey, this.resourceTokens); + try { + authorization = URLEncoder.encode(authorization, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Failed to encode authtoken.", e); + } + request.getHeaders().put(HttpConstants.HttpHeaders.AUTHORIZATION, authorization); + } + + if ((HttpConstants.HttpMethods.POST.equals(httpMethod) || HttpConstants.HttpMethods.PUT.equals(httpMethod)) + && !request.getHeaders().containsKey(HttpConstants.HttpHeaders.CONTENT_TYPE)) { + request.getHeaders().put(HttpConstants.HttpHeaders.CONTENT_TYPE, RuntimeConstants.MediaTypes.JSON); + } + + if (!request.getHeaders().containsKey(HttpConstants.HttpHeaders.ACCEPT)) { + request.getHeaders().put(HttpConstants.HttpHeaders.ACCEPT, RuntimeConstants.MediaTypes.JSON); + } + } + + private String getAuthorizationToken(String resourceOrOwnerId, String path, ResourceType resourceType, + String requestVerb, Map headers, String masterKey, Map resourceTokens) { + if (masterKey != null) { + return this.authorizationTokenProvider.generateKeyAuthorizationSignature(requestVerb, resourceOrOwnerId, resourceType, headers); + } else { + assert resourceTokens != null; + return this.authorizationTokenProvider.getAuthorizationTokenUsingResourceTokens(resourceTokens, path, resourceOrOwnerId); + } + } + + private void applySessionToken(RxDocumentServiceRequest request) { + Map headers = request.getHeaders(); + if (headers != null && !StringUtils.isEmpty(headers.get(HttpConstants.HttpHeaders.SESSION_TOKEN))) { + return; // User is explicitly controlling the session. + } + + String requestConsistency = request.getHeaders().get(HttpConstants.HttpHeaders.CONSISTENCY_LEVEL); + boolean sessionConsistency = this.consistencyLevel == ConsistencyLevel.Session || + (!StringUtils.isEmpty(requestConsistency) && StringUtils.equalsIgnoreCase(requestConsistency, ConsistencyLevel.Session.toString())); + if (!sessionConsistency) { + return; // Only apply the session token in case of session consistency + } + + // Apply the ambient session. + if (!StringUtils.isEmpty(request.getResourceAddress())) { + String sessionToken = this.sessionContainer.resolveSessionToken(request); + + if (!StringUtils.isEmpty(sessionToken)) { + headers.put(HttpConstants.HttpHeaders.SESSION_TOKEN, sessionToken); + } + } + } + + void captureSessionToken(RxDocumentServiceRequest request, DocumentServiceResponse response) { + this.sessionContainer.setSessionToken(request, response); + } + + void clearToken(RxDocumentServiceRequest request, DocumentServiceResponse response) { + this.sessionContainer.clearToken(request); + } + + private Observable doCreate(RxDocumentServiceRequest request) { + + Observable responseObservable = + Observable.defer(() -> { + try { + return this.gatewayProxy.processMessage(request) + .doOnNext(response -> { + this.captureSessionToken(request, response); + }); + } catch (Exception e) { + return Observable.error(e); + } + }) + .retryWhen(createExecuteRequestRetryHandler(request)); + + return createPutMoreContentObservable(request, HttpConstants.HttpMethods.POST) + .doOnNext(r -> applySessionToken(request)).flatMap(req -> responseObservable); + + } + + /** + * Creates an observable which does the CPU intensive operation of generating authentication token and putting more content in the request + * + * This observable runs on computationScheduler + * @param request + * @param method + * @return + */ + private Observable createPutMoreContentObservable(RxDocumentServiceRequest request, String method) { + return Observable.create(s -> { + try { + putMoreContentIntoDocumentServiceRequest(request, method); + s.onNext(request); + s.onCompleted(); + } catch (Exception e) { + s.onError(e); + } + }).subscribeOn(this.computationScheduler); + } + + private Observable doUpsert(RxDocumentServiceRequest request) { + + Observable responseObservable = Observable.defer(() -> { + try { + return this.gatewayProxy.processMessage(request).doOnNext(response -> { + this.captureSessionToken(request, response); + }); + } catch (Exception e) { + return Observable.error(e); + } + }).retryWhen(createExecuteRequestRetryHandler(request)); + + return createPutMoreContentObservable(request, HttpConstants.HttpMethods.POST) + .doOnNext(r -> { + applySessionToken(request); + Map headers = request.getHeaders(); + // headers can never be null, since it will be initialized even when no + // request options are specified, + // hence using assertion here instead of exception, being in the private + // method + assert (headers != null); + headers.put(HttpConstants.HttpHeaders.IS_UPSERT, "true"); + + }) + .flatMap(req -> responseObservable); + } + + private Observable doReplace(RxDocumentServiceRequest request) { + + + Observable responseObservable = Observable.defer(() -> { + try { + return this.gatewayProxy.doReplace(request).doOnNext(response -> { + this.captureSessionToken(request, response); + }); + } catch (Exception e) { + return Observable.error(e); + } + }).retryWhen(createExecuteRequestRetryHandler(request)); + + + return createPutMoreContentObservable(request, HttpConstants.HttpMethods.PUT) + .doOnNext(r -> applySessionToken(request)).flatMap(req -> responseObservable); + + } + + @Override + public Observable> createDocument(String + collectionLink, Object document, + RequestOptions options, boolean disableAutomaticIdGeneration) { + + return Observable.defer(() -> { + + try { + logger.debug("Creating a Document. collectionLink: [{}]", + collectionLink); + + final String documentCollectionLink = + this.getTargetDocumentCollectionLink(collectionLink, document); + final Object documentLocal = document; + final RequestOptions optionsLocal = options; + final boolean disableAutomaticIdGenerationLocal = disableAutomaticIdGeneration; + final boolean shouldRetry = options == null || options.getPartitionKey() == null; + Observable> createObservable = + Observable.defer(() -> { + RxDocumentServiceRequest request = + getCreateDocumentRequest(documentCollectionLink, documentLocal, + optionsLocal, + disableAutomaticIdGenerationLocal, OperationType.Create); + + Observable responseObservable = + this.doCreate(request); + return responseObservable + .map(serviceResponse -> toResourceResponse(serviceResponse, + Document.class)); + }); + + if (shouldRetry) { + CreateDocumentRetryHandler createDocumentRetryHandler = new CreateDocumentRetryHandler(this.collectionCache, documentCollectionLink); + return createObservable.retryWhen(RetryFunctionFactory.from(createDocumentRetryHandler)); + } else { + return createObservable; + } + + } catch (Exception e) { + logger.debug("Failure in creating a document due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> upsertDocument(String collectionLink, Object document, + RequestOptions options, boolean disableAutomaticIdGeneration) { + return Observable.defer(() -> { + try { + logger.debug("Upserting a Document. collectionLink: [{}]", collectionLink); + final String documentCollectionLink = this.getTargetDocumentCollectionLink(collectionLink, document); + final Object documentLocal = document; + final RequestOptions optionsLocal = options; + final boolean disableAutomaticIdGenerationLocal = disableAutomaticIdGeneration; + final boolean shouldRetry = options == null || options.getPartitionKey() == null; + + Observable> upsertObservable = + Observable.defer(() -> { + + RxDocumentServiceRequest request = getCreateDocumentRequest(documentCollectionLink, + documentLocal, optionsLocal, disableAutomaticIdGenerationLocal, + OperationType.Upsert); + + Observable responseObservable = + this.doUpsert(request); + return responseObservable + .map(serviceResponse -> toResourceResponse(serviceResponse, + Document.class)); + }); + + if (shouldRetry) { + CreateDocumentRetryHandler createDocumentRetryHandler = new CreateDocumentRetryHandler(this.collectionCache, documentCollectionLink); + return upsertObservable.retryWhen(RetryFunctionFactory.from(createDocumentRetryHandler)); + } else { + return upsertObservable; + } + + } catch (Exception e) { + logger.debug("Failure in upserting a document due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> replaceDocument(String documentLink, Object document, + RequestOptions options) { + return Observable.defer(() -> { + + try { + if (StringUtils.isEmpty(documentLink)) { + throw new IllegalArgumentException("documentLink"); + } + + if (document == null) { + throw new IllegalArgumentException("document"); + } + + Document typedDocument = documentFromObject(document); + + return this.replaceDocumentInternal(documentLink, typedDocument, options); + + } catch (Exception e) { + logger.debug("Failure in replacing a document due to [{}]", e.getMessage()); + return Observable.error(e); + } + }); + } + + @Override + public Observable> replaceDocument(Document document, RequestOptions options) { + + return Observable.defer(() -> { + + try { + if (document == null) { + throw new IllegalArgumentException("document"); + } + + return this.replaceDocumentInternal(document.getSelfLink(), document, options); + + } catch (Exception e) { + logger.debug("Failure in replacing a database due to [{}]", e.getMessage()); + return Observable.error(e); + } + }); + } + + private Observable> replaceDocumentInternal(String documentLink, Document document, + RequestOptions options) throws DocumentClientException { + + if (document == null) { + throw new IllegalArgumentException("document"); + } + + logger.debug("Replacing a Document. documentLink: [{}]", documentLink); + final String documentCollectionName = Utils.getCollectionName(documentLink); + final String documentCollectionLink = this.getTargetDocumentCollectionLink(documentCollectionName, document); + final String path = Utils.joinPath(documentLink, null); + final Map requestHeaders = getRequestHeaders(options); + final RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Replace, + ResourceType.Document, path, document, requestHeaders); + + final boolean shouldRetry = options == null || options.getPartitionKey() == null; + + // NOTE: if the collection is not cached this will block till collection + // is retrieved + DocumentCollection collection = this.collectionCache.resolveCollection(request); + + this.addPartitionKeyInformation(request, document, options, collection); + validateResource(document); + + Observable> resourceResponseObs = this.doReplace(request) + .map(resp -> toResourceResponse(resp, Document.class)); + + if (shouldRetry) { + CreateDocumentRetryHandler createDocumentRetryHandler = new CreateDocumentRetryHandler(null, + documentCollectionLink); + return resourceResponseObs.retryWhen(RetryFunctionFactory.from(createDocumentRetryHandler)); + } else { + return resourceResponseObs; + } + } + + @Override + public Observable> deleteDocument(String documentLink, RequestOptions options) { + return Observable.defer(() -> { + + try { + if (StringUtils.isEmpty(documentLink)) { + throw new IllegalArgumentException("documentLink"); + } + + logger.debug("Deleting a Document. documentLink: [{}]", documentLink); + String path = Utils.joinPath(documentLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Delete, + ResourceType.Document, path, requestHeaders); + + // NOTE: if collection is not cached, this will block till + // collection is retrieved + DocumentCollection collection = this.collectionCache.resolveCollection(request); + + this.addPartitionKeyInformation(request, null, options, collection); + + Observable responseObservable = this.doDelete(request); + return responseObservable.map(serviceResponse -> toResourceResponse(serviceResponse, Document.class)); + + } catch (Exception e) { + logger.debug("Failure in deleting a document due to [{}]", e.getMessage()); + return Observable.error(e); + } + }); + } + + @Override + public Observable> readDocument(String documentLink, RequestOptions options) { + return Observable.defer(() -> { + + try { + if (StringUtils.isEmpty(documentLink)) { + throw new IllegalArgumentException("documentLink"); + } + + logger.debug("Reading a Document. documentLink: [{}]", documentLink); + String path = Utils.joinPath(documentLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Read, + ResourceType.Document, path, requestHeaders); + + // NOTE: if the collection is not cached, this will block till + // collection is retrieved + DocumentCollection collection = this.collectionCache.resolveCollection(request); + + this.addPartitionKeyInformation(request, null, options, collection); + + Observable responseObservable = this.doRead(request); + return responseObservable.map(serviceResponse -> toResourceResponse(serviceResponse, Document.class)); + + } catch (Exception e) { + logger.debug("Failure in reading a document due to [{}]", e.getMessage()); + return Observable.error(e); + } + }); + } + + @Override + public Observable> readDocuments(String collectionLink, FeedOptions options) { + return this.rxWrapperClient.readDocuments(collectionLink, options); + } + + @Override + public Observable> queryDocuments(String collectionLink, String query, + FeedOptions options) { + return this.rxWrapperClient.queryDocuments(collectionLink, query, options); + } + + @Override + public Observable> queryDocuments(String collectionLink, String query, + FeedOptions options, Object partitionKey) { + return this.rxWrapperClient.queryDocuments(collectionLink, query, options, partitionKey); + } + + @Override + public Observable> queryDocuments(String collectionLink, SqlQuerySpec querySpec, + FeedOptions options) { + return this.rxWrapperClient.queryDocuments(collectionLink, querySpec, options); + } + + @Override + public Observable> queryDocuments(String collectionLink, SqlQuerySpec querySpec, + FeedOptions options, Object partitionKey) { + return this.rxWrapperClient.queryDocuments(collectionLink, querySpec, options, partitionKey); + } + + private RxDocumentServiceRequest getStoredProcedureRequest(String collectionLink, StoredProcedure storedProcedure, + RequestOptions options, OperationType operationType) { + if (StringUtils.isEmpty(collectionLink)) { + throw new IllegalArgumentException("collectionLink"); + } + if (storedProcedure == null) { + throw new IllegalArgumentException("storedProcedure"); + } + + validateResource(storedProcedure); + + String path = Utils.joinPath(collectionLink, Paths.STORED_PROCEDURES_PATH_SEGMENT); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(operationType, ResourceType.StoredProcedure, + path, storedProcedure, requestHeaders); + return request; + } + + private RxDocumentServiceRequest getUserDefinedFunctionRequest(String collectionLink, UserDefinedFunction udf, + RequestOptions options, OperationType operationType) { + if (StringUtils.isEmpty(collectionLink)) { + throw new IllegalArgumentException("collectionLink"); + } + if (udf == null) { + throw new IllegalArgumentException("udf"); + } + + validateResource(udf); + + String path = Utils.joinPath(collectionLink, Paths.USER_DEFINED_FUNCTIONS_PATH_SEGMENT); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(operationType, + ResourceType.UserDefinedFunction, path, udf, requestHeaders); + return request; + } + + @Override + public Observable> createStoredProcedure(String collectionLink, + StoredProcedure storedProcedure, RequestOptions options) { + + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + + logger.debug("Creating a StoredProcedure. collectionLink: [{}], storedProcedure id [{}]", + collectionLink, storedProcedure.getId()); + RxDocumentServiceRequest request = getStoredProcedureRequest(collectionLink, storedProcedure, options, + OperationType.Create); + + return this.doCreate(request).map(response -> toResourceResponse(response, StoredProcedure.class)); + + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in creating a StoredProcedure due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> upsertStoredProcedure(String collectionLink, + StoredProcedure storedProcedure, RequestOptions options) { + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + + logger.debug("Upserting a StoredProcedure. collectionLink: [{}], storedProcedure id [{}]", + collectionLink, storedProcedure.getId()); + RxDocumentServiceRequest request = getStoredProcedureRequest(collectionLink, storedProcedure, options, + OperationType.Upsert); + + return this.doUpsert(request).map(response -> toResourceResponse(response, StoredProcedure.class)); + + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in upserting a StoredProcedure due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> replaceStoredProcedure(StoredProcedure storedProcedure, + RequestOptions options) { + + return this.rxWrapperClient.replaceStoredProcedure(storedProcedure, options); + } + + @Override + public Observable> deleteStoredProcedure(String storedProcedureLink, + RequestOptions options) { + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + + if (StringUtils.isEmpty(storedProcedureLink)) { + throw new IllegalArgumentException("storedProcedureLink"); + } + + logger.debug("Deleting a StoredProcedure. storedProcedureLink [{}]", storedProcedureLink); + String path = Utils.joinPath(storedProcedureLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Delete, + ResourceType.StoredProcedure, path, requestHeaders); + + return this.doDelete(request).map(response -> toResourceResponse(response, StoredProcedure.class)); + + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in deleting a StoredProcedure due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> readStoredProcedure(String storedProcedureLink, + RequestOptions options) { + + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + + if (StringUtils.isEmpty(storedProcedureLink)) { + throw new IllegalArgumentException("storedProcedureLink"); + } + + logger.debug("Reading a StoredProcedure. storedProcedureLink [{}]", storedProcedureLink); + String path = Utils.joinPath(storedProcedureLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Read, + ResourceType.StoredProcedure, path, requestHeaders); + + return this.doRead(request).map(response -> toResourceResponse(response, StoredProcedure.class)); + + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in reading a StoredProcedure due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + + } + + @Override + public Observable> readStoredProcedures(String collectionLink, + FeedOptions options) { + return this.rxWrapperClient.readStoredProcedures(collectionLink, options); + } + + @Override + public Observable> queryStoredProcedures(String collectionLink, String query, + FeedOptions options) { + return this.rxWrapperClient.queryStoredProcedures(collectionLink, query, options); + } + + @Override + public Observable> queryStoredProcedures(String collectionLink, + SqlQuerySpec querySpec, FeedOptions options) { + return this.rxWrapperClient.queryStoredProcedures(collectionLink, querySpec, options); + } + + @Override + public Observable executeStoredProcedure(String storedProcedureLink, + Object[] procedureParams) { + return this.rxWrapperClient.executeStoredProcedure(storedProcedureLink, procedureParams); + } + + @Override + public Observable executeStoredProcedure(String storedProcedureLink, + RequestOptions options, Object[] procedureParams) { + return this.rxWrapperClient.executeStoredProcedure(storedProcedureLink, options, procedureParams); + } + + @Override + public Observable> createTrigger(String collectionLink, Trigger trigger, + RequestOptions options) { + return this.rxWrapperClient.createTrigger(collectionLink, trigger, options); + } + + @Override + public Observable> upsertTrigger(String collectionLink, Trigger trigger, + RequestOptions options) { + return this.rxWrapperClient.upsertTrigger(collectionLink, trigger, options); + } + + @Override + public Observable> replaceTrigger(Trigger trigger, RequestOptions options) { + return this.rxWrapperClient.replaceTrigger(trigger, options); + } + + @Override + public Observable> deleteTrigger(String triggerLink, RequestOptions options) { + return this.rxWrapperClient.deleteTrigger(triggerLink, options); + } + + @Override + public Observable> readTrigger(String triggerLink, RequestOptions options) { + return this.rxWrapperClient.readTrigger(triggerLink, options); + } + + @Override + public Observable> readTriggers(String collectionLink, FeedOptions options) { + return this.rxWrapperClient.readTriggers(collectionLink, options); + } + + @Override + public Observable> queryTriggers(String collectionLink, String query, + FeedOptions options) { + return this.rxWrapperClient.queryTriggers(collectionLink, query, options); + } + + @Override + public Observable> queryTriggers(String collectionLink, SqlQuerySpec querySpec, + FeedOptions options) { + return this.rxWrapperClient.queryTriggers(collectionLink, querySpec, options); + } + + @Override + public Observable> createUserDefinedFunction(String collectionLink, + UserDefinedFunction udf, RequestOptions options) { + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + logger.debug("Creating a UserDefinedFunction. collectionLink [{}], udf id [{}]", collectionLink, + udf.getId()); + RxDocumentServiceRequest request = getUserDefinedFunctionRequest(collectionLink, udf, options, + OperationType.Create); + + return this.doCreate(request).map(response -> toResourceResponse(response, UserDefinedFunction.class)); + + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in creating a UserDefinedFunction due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> upsertUserDefinedFunction(String collectionLink, + UserDefinedFunction udf, RequestOptions options) { + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + logger.debug("Upserting a UserDefinedFunction. collectionLink [{}], udf id [{}]", collectionLink, + udf.getId()); + RxDocumentServiceRequest request = getUserDefinedFunctionRequest(collectionLink, udf, options, + OperationType.Upsert); + return this.doUpsert(request).map(response -> toResourceResponse(response, UserDefinedFunction.class)); + + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in upserting a UserDefinedFunction due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> replaceUserDefinedFunction(UserDefinedFunction udf, + RequestOptions options) { + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + if (udf == null) { + throw new IllegalArgumentException("udf"); + } + + logger.debug("Replacing a UserDefinedFunction. udf id [{}]", udf.getId()); + validateResource(udf); + + String path = Utils.joinPath(udf.getSelfLink(), null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Replace, + ResourceType.UserDefinedFunction, path, udf, requestHeaders); + return this.doReplace(request).map(response -> toResourceResponse(response, UserDefinedFunction.class)); + + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in replacing a UserDefinedFunction due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> deleteUserDefinedFunction(String udfLink, + RequestOptions options) { + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + if (StringUtils.isEmpty(udfLink)) { + throw new IllegalArgumentException("udfLink"); + } + + logger.debug("Deleting a UserDefinedFunction. udfLink [{}]", udfLink); + String path = Utils.joinPath(udfLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Delete, + ResourceType.UserDefinedFunction, path, requestHeaders); + return this.doDelete(request).map(response -> toResourceResponse(response, UserDefinedFunction.class)); + + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in deleting a UserDefinedFunction due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> readUserDefinedFunction(String udfLink, + RequestOptions options) { + return Observable.defer(() -> { + // we are using an observable factory here + // observable will be created fresh upon subscription + // this is to ensure we capture most up to date information (e.g., + // session) + try { + if (StringUtils.isEmpty(udfLink)) { + throw new IllegalArgumentException("udfLink"); + } + + logger.debug("Reading a UserDefinedFunction. udfLink [{}]", udfLink); + String path = Utils.joinPath(udfLink, null); + Map requestHeaders = this.getRequestHeaders(options); + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Read, + ResourceType.UserDefinedFunction, path, requestHeaders); + + return this.doRead(request).map(response -> toResourceResponse(response, UserDefinedFunction.class)); + + } catch (Exception e) { + // this is only in trace level to capture what's going on + logger.debug("Failure in reading a UserDefinedFunction due to [{}]", e.getMessage(), e); + return Observable.error(e); + } + }); + } + + @Override + public Observable> readUserDefinedFunctions(String collectionLink, + FeedOptions options) { + return this.rxWrapperClient.readUserDefinedFunctions(collectionLink, options); + } + + @Override + public Observable> queryUserDefinedFunctions(String collectionLink, + String query, FeedOptions options) { + return this.rxWrapperClient.queryUserDefinedFunctions(collectionLink, query, options); + } + + @Override + public Observable> queryUserDefinedFunctions(String collectionLink, + SqlQuerySpec querySpec, FeedOptions options) { + return this.rxWrapperClient.queryUserDefinedFunctions(collectionLink, querySpec, options); + } + + @Override + public Observable> createAttachment(String documentLink, Attachment attachment, + RequestOptions options) { + return this.rxWrapperClient.createAttachment(documentLink, attachment, options); + } + + @Override + public Observable> upsertAttachment(String documentLink, Attachment attachment, + RequestOptions options) { + return this.rxWrapperClient.upsertAttachment(documentLink, attachment, options); + } + + @Override + public Observable> replaceAttachment(Attachment attachment, RequestOptions options) { + return this.rxWrapperClient.replaceAttachment(attachment, options); + } + + @Override + public Observable> deleteAttachment(String attachmentLink, RequestOptions options) { + return this.rxWrapperClient.deleteAttachment(attachmentLink, options); + } + + @Override + public Observable> readAttachment(String attachmentLink, RequestOptions options) { + return this.rxWrapperClient.readAttachment(attachmentLink, options); + } + + @Override + public Observable> readAttachments(String documentLink, FeedOptions options) { + return this.rxWrapperClient.readAttachments(documentLink, options); + } + + @Override + public Observable> queryAttachments(String documentLink, String query, + FeedOptions options) { + return this.rxWrapperClient.queryAttachments(documentLink, query, options); + } + + @Override + public Observable> queryAttachments(String documentLink, SqlQuerySpec querySpec, + FeedOptions options) { + return this.rxWrapperClient.queryAttachments(documentLink, querySpec, options); + } + + @Override + public Observable> createAttachment(String documentLink, InputStream mediaStream, + MediaOptions options) { + return this.rxWrapperClient.createAttachment(documentLink, mediaStream, options); + } + + @Override + public Observable> upsertAttachment(String documentLink, InputStream mediaStream, + MediaOptions options) { + return this.rxWrapperClient.upsertAttachment(documentLink, mediaStream, options); + } + + @Override + public Observable readMedia(String mediaLink) { + return this.rxWrapperClient.readMedia(mediaLink); + } + + @Override + public Observable updateMedia(String mediaLink, InputStream mediaStream, MediaOptions options) { + return this.rxWrapperClient.updateMedia(mediaLink, mediaStream, options); + } + + @Override + public Observable> readConflict(String conflictLink, RequestOptions options) { + return this.rxWrapperClient.readConflict(conflictLink, options); + } + + @Override + public Observable> readConflicts(String collectionLink, FeedOptions options) { + return this.rxWrapperClient.readConflicts(collectionLink, options); + } + + @Override + public Observable> queryConflicts(String collectionLink, String query, + FeedOptions options) { + return this.rxWrapperClient.queryConflicts(collectionLink, query, options); + } + + @Override + public Observable> queryConflicts(String collectionLink, SqlQuerySpec querySpec, + FeedOptions options) { + return this.rxWrapperClient.queryConflicts(collectionLink, querySpec, options); + } + + @Override + public Observable> deleteConflict(String conflictLink, RequestOptions options) { + return this.rxWrapperClient.deleteConflict(conflictLink, options); + } + + @Override + public Observable> createUser(String databaseLink, User user, RequestOptions options) { + return this.rxWrapperClient.createUser(databaseLink, user, options); + } + + @Override + public Observable> upsertUser(String databaseLink, User user, RequestOptions options) { + return this.rxWrapperClient.upsertUser(databaseLink, user, options); + } + + @Override + public Observable> replaceUser(User user, RequestOptions options) { + return this.rxWrapperClient.replaceUser(user, options); + } + + @Override + public Observable> deleteUser(String userLink, RequestOptions options) { + return this.rxWrapperClient.deleteUser(userLink, options); + } + + @Override + public Observable> readUser(String userLink, RequestOptions options) { + return this.rxWrapperClient.readUser(userLink, options); + } + + @Override + public Observable> readUsers(String databaseLink, FeedOptions options) { + return this.rxWrapperClient.readUsers(databaseLink, options); + } + + @Override + public Observable> queryUsers(String databaseLink, String query, FeedOptions options) { + return this.rxWrapperClient.queryUsers(databaseLink, query, options); + } + + @Override + public Observable> queryUsers(String databaseLink, SqlQuerySpec querySpec, + FeedOptions options) { + return this.rxWrapperClient.queryUsers(databaseLink, querySpec, options); + } + + @Override + public Observable> createPermission(String userLink, Permission permission, + RequestOptions options) { + return this.rxWrapperClient.createPermission(userLink, permission, options); + } + + @Override + public Observable> upsertPermission(String userLink, Permission permission, + RequestOptions options) { + return this.rxWrapperClient.upsertPermission(userLink, permission, options); + } + + @Override + public Observable> replacePermission(Permission permission, RequestOptions options) { + return this.rxWrapperClient.replacePermission(permission, options); + } + + @Override + public Observable> deletePermission(String permissionLink, RequestOptions options) { + return this.rxWrapperClient.deletePermission(permissionLink, options); + } + + @Override + public Observable> readPermission(String permissionLink, RequestOptions options) { + return this.rxWrapperClient.readPermission(permissionLink, options); + } + + @Override + public Observable> readPermissions(String permissionLink, FeedOptions options) { + return this.rxWrapperClient.readPermissions(permissionLink, options); + } + + @Override + public Observable> queryPermissions(String permissionLink, String query, + FeedOptions options) { + return this.rxWrapperClient.queryPermissions(permissionLink, query, options); + } + + @Override + public Observable> queryPermissions(String permissionLink, SqlQuerySpec querySpec, + FeedOptions options) { + return this.rxWrapperClient.queryPermissions(permissionLink, querySpec, options); + } + + @Override + public Observable> replaceOffer(Offer offer) { + return this.rxWrapperClient.replaceOffer(offer); + } + + @Override + public Observable> readOffer(String offerLink) { + return this.rxWrapperClient.readOffer(offerLink); + } + + @Override + public Observable> readOffers(FeedOptions options) { + return this.rxWrapperClient.readOffers(options); + } + + @Override + public Observable> queryOffers(String query, FeedOptions options) { + return this.rxWrapperClient.queryOffers(query, options); + } + + @Override + public Observable> queryOffers(SqlQuerySpec querySpec, FeedOptions options) { + return this.rxWrapperClient.queryOffers(querySpec, options); + } + + @Override + public Observable getDatabaseAccount() { + return this.rxWrapperClient.getDatabaseAccount(); + } + + public Observable getDatabaseAccountFromEndpoint(URI endpoint) { + return Observable.defer(() -> { + RxDocumentServiceRequest request = RxDocumentServiceRequest.create(OperationType.Read, + ResourceType.DatabaseAccount, "", null); + this.putMoreContentIntoDocumentServiceRequest(request, HttpConstants.HttpMethods.GET); + + request.setEndpointOverride(endpoint); + return this.gatewayProxy.doRead(request).doOnError(e -> { + String message = "Failed to retrieve database account information. %s"; + Throwable cause = e.getCause(); + if (cause != null) { + message = String.format(message, cause.toString()); + } else { + message = String.format(message, e.toString()); + } + logger.warn(message); + }).map(rsp -> rsp.getResource(DatabaseAccount.class)); + }); + } + + private void safeShutdownExecutorService(ExecutorService exS) { + if (exS == null) { + return; + } + + try { + exS.shutdown(); + exS.awaitTermination(15, TimeUnit.SECONDS); + } catch (Exception e) { + logger.warn("Failure in shutting down a executor service", e); + } + } + + @Override + public void close() { + + this.safeShutdownExecutorService(this.collectionCacheExecutorService); + this.safeShutdownExecutorService(this.computationExecutor); + + try { + this.rxWrapperClient.close(); + } catch (Exception e) { + logger.warn("Failure in shutting down rxWrapperClient", e); + } + + try { + this.rxClient.shutdown(); + } catch (Exception e) { + logger.warn("Failure in shutting down rxClient", e); + } + } + + private Func1, Observable> createExecuteRequestRetryHandler( + RxDocumentServiceRequest request) { + return RetryFunctionFactory + .from(new ExecuteDocumentClientRequestRetryHandler(request, globalEndpointManager, this)); + } + +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxDocumentServiceRequest.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxDocumentServiceRequest.java new file mode 100644 index 000000000000..7be0a5194958 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxDocumentServiceRequest.java @@ -0,0 +1,278 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import com.microsoft.azure.documentdb.Resource; +import com.microsoft.azure.documentdb.SqlQuerySpec; +import com.microsoft.azure.documentdb.internal.AbstractDocumentServiceRequest; +import com.microsoft.azure.documentdb.internal.OperationType; +import com.microsoft.azure.documentdb.internal.PathsHelper; +import com.microsoft.azure.documentdb.internal.QueryCompatibilityMode; +import com.microsoft.azure.documentdb.internal.ResourceType; +import com.microsoft.azure.documentdb.internal.Utils; + +import rx.Observable; +import rx.observables.StringObservable; + +/** + * This is core Transport/Connection agnostic request to the Azure DocumentDB database service. + */ +class RxDocumentServiceRequest extends AbstractDocumentServiceRequest { + + private final Observable contentObservable; + private final byte[] byteContent; + + /** + * Creates a DocumentServiceRequest + * + * @param resourceId the resource Id. + * @param resourceType the resource type. + * @param content the byte content observable\ + * @param contentObservable the byte content observable + * @param headers the request headers. + */ + private RxDocumentServiceRequest(OperationType operationType, + String resourceId, + ResourceType resourceType, + Observable contentObservable, + byte[] content, + String path, + Map headers) { + super( operationType, + resourceId, + resourceType, + path, + headers); + this.byteContent = content; + this.contentObservable = null; + } + + /** + * Creates a DocumentServiceRequest with an HttpEntity. + * + * @param resourceType the resource type. + * @param path the relative URI path. + * @param contentObservable the byte content observable + * @param headers the request headers. + */ + private RxDocumentServiceRequest(OperationType operationType, + ResourceType resourceType, + String path, + Observable contentObservable, + Map headers) { + this(operationType, extractIdFromUri(path), resourceType, contentObservable, null, path, headers); + } + + /** + * Creates a DocumentServiceRequest with an HttpEntity. + * + * @param resourceType the resource type. + * @param path the relative URI path. + * @param byteContent the byte content. + * @param headers the request headers. + */ + private RxDocumentServiceRequest(OperationType operationType, + ResourceType resourceType, + String path, + byte[] byteContent, + Map headers) { + this(operationType, extractIdFromUri(path), resourceType, null, byteContent, path, headers); + } + + /** + * Creates a DocumentServiceRequest with an HttpEntity. + * + * @param resourceType the resource type. + * @param path the relative URI path. + * @param headers the request headers. + */ + private RxDocumentServiceRequest(OperationType operationType, + ResourceType resourceType, + String path, + Map headers) { + this(operationType, extractIdFromUri(path), resourceType, null , null, path, headers); + } + + /** + * Creates a DocumentServiceRequest with a stream. + * + * @param operation the operation type. + * @param resourceType the resource type. + * @param relativePath the relative URI path. + * @param content the content observable + * @param headers the request headers. + * @return the created document service request. + */ + public static RxDocumentServiceRequest create(OperationType operation, + ResourceType resourceType, + String relativePath, + Observable content, + Map headers) { + return new RxDocumentServiceRequest(operation, resourceType, relativePath, content, headers); + } + + /** + * Creates a DocumentServiceRequest with a stream. + * + * @param operation the operation type. + * @param resourceType the resource type. + * @param relativePath the relative URI path. + * @param inputStream the input stream. + * @param headers the request headers. + * @return the created document service request. + */ + public static RxDocumentServiceRequest create(OperationType operation, + ResourceType resourceType, + String relativePath, + InputStream inputStream, + Map headers) { + return new RxDocumentServiceRequest(operation, resourceType, relativePath, StringObservable.from(inputStream), headers); + } + + /** + * Creates a DocumentServiceRequest with a resource. + * + * @param operation the operation type. + * @param resourceType the resource type. + * @param relativePath the relative URI path. + * @param resource the resource of the request. + * @param headers the request headers. + * @return the created document service request. + */ + public static RxDocumentServiceRequest create(OperationType operation, + ResourceType resourceType, + String relativePath, + Resource resource, + Map headers) { + + return new RxDocumentServiceRequest(operation, resourceType, relativePath, + // TODO: this re-encodes, can we improve performance here? + resource.toJson().getBytes(StandardCharsets.UTF_8), headers); + } + + /** + * Creates a DocumentServiceRequest with a query. + * + * @param operation the operation type. + * @param resourceType the resource type. + * @param relativePath the relative URI path. + * @param query the query. + * @param headers the request headers. + * @return the created document service request. + */ + public static RxDocumentServiceRequest create(OperationType operation, + ResourceType resourceType, + String relativePath, + String query, + Map headers) { + + return new RxDocumentServiceRequest(operation, resourceType, relativePath, + query.getBytes(StandardCharsets.UTF_8), headers); + } + + /** + * Creates a DocumentServiceRequest with a query. + * + * @param resourceType the resource type. + * @param relativePath the relative URI path. + * @param querySpec the query. + * @param queryCompatibilityMode the QueryCompatibilityMode mode. + * @param headers the request headers. + * @return the created document service request. + */ + public static RxDocumentServiceRequest create(ResourceType resourceType, + String relativePath, + SqlQuerySpec querySpec, + QueryCompatibilityMode queryCompatibilityMode, + Map headers) { + OperationType operation; + String queryText; + switch (queryCompatibilityMode) { + case SqlQuery: + if (querySpec.getParameters() != null && querySpec.getParameters().size() > 0) { + throw new IllegalArgumentException( + String.format("Unsupported argument in query compatibility mode '{%s}'", + queryCompatibilityMode.name())); + } + + operation = OperationType.SqlQuery; + queryText = querySpec.getQueryText(); + break; + + case Default: + case Query: + default: + operation = OperationType.Query; + queryText = querySpec.toJson(); + break; + } + + Observable body = StringObservable.encode(Observable.just(queryText), StandardCharsets.UTF_8); + return new RxDocumentServiceRequest(operation, resourceType, relativePath, body, headers); + } + + /** + * Creates a DocumentServiceRequest without body. + * + * @param operation the operation type. + * @param resourceType the resource type. + * @param relativePath the relative URI path. + * @param headers the request headers. + * @return the created document service request. + */ + public static RxDocumentServiceRequest create(OperationType operation, + ResourceType resourceType, + String relativePath, + Map headers) { + return new RxDocumentServiceRequest(operation, resourceType, relativePath, headers); + } + + /** + * Creates a DocumentServiceRequest with a resourceId. + * + * @param operation the operation type. + * @param resourceId the resource id. + * @param resourceType the resource type. + * @param headers the request headers. + * @return the created document service request. + */ + public static RxDocumentServiceRequest create(OperationType operation, + String resourceId, + ResourceType resourceType, + Map headers) { + String path = PathsHelper.generatePath(resourceType, resourceId, Utils.isFeedRequest(operation)); + return new RxDocumentServiceRequest(operation, resourceId, resourceType, null, null, path, headers); + } + + public Observable getContentObservable() { + return contentObservable; + } + + public byte[] getContent() { + return byteContent; + } +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxGatewayStoreModel.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxGatewayStoreModel.java new file mode 100644 index 000000000000..54b29f184647 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxGatewayStoreModel.java @@ -0,0 +1,395 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.io.IOUtils; +import org.apache.http.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.Error; +import com.microsoft.azure.documentdb.internal.DocumentServiceResponse; +import com.microsoft.azure.documentdb.internal.EndpointManager; +import com.microsoft.azure.documentdb.internal.HttpConstants; +import com.microsoft.azure.documentdb.internal.OperationType; +import com.microsoft.azure.documentdb.internal.QueryCompatibilityMode; +import com.microsoft.azure.documentdb.internal.RuntimeConstants; +import com.microsoft.azure.documentdb.internal.UserAgentContainer; +import com.microsoft.azure.documentdb.internal.directconnectivity.StoreResponse; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.reactivex.netty.protocol.http.client.HttpClient; +import io.reactivex.netty.protocol.http.client.HttpClientRequest; +import io.reactivex.netty.protocol.http.client.HttpClientResponse; +import io.reactivex.netty.protocol.http.client.HttpResponseHeaders; +import rx.Observable; +import rx.exceptions.Exceptions; + +/** + * Used internally to provide functionality to communicate and process response from Gateway in the Azure DocumentDB database service. + */ +class RxGatewayStoreModel implements RxStoreModel { + + private final Logger logger = LoggerFactory.getLogger(RxGatewayStoreModel.class); + private final Map defaultHeaders; + private final HttpClient httpClient; + private final QueryCompatibilityMode queryCompatibilityMode; + private final EndpointManager globalEndpointManager; + + public RxGatewayStoreModel(ConnectionPolicy connectionPolicy, + ConsistencyLevel consistencyLevel, + QueryCompatibilityMode queryCompatibilityMode, + String masterKey, + Map resourceTokens, + UserAgentContainer userAgentContainer, + EndpointManager globalEndpointManager, + HttpClient httpClient) { + this.defaultHeaders = new HashMap(); + this.defaultHeaders.put(HttpConstants.HttpHeaders.CACHE_CONTROL, + "no-cache"); + this.defaultHeaders.put(HttpConstants.HttpHeaders.VERSION, + HttpConstants.Versions.CURRENT_VERSION); + + if (userAgentContainer == null) { + userAgentContainer = new UserAgentContainer(); + } + + this.defaultHeaders.put(HttpConstants.HttpHeaders.USER_AGENT, userAgentContainer.getUserAgent()); + + if (consistencyLevel != null) { + this.defaultHeaders.put(HttpConstants.HttpHeaders.CONSISTENCY_LEVEL, + consistencyLevel.toString()); + } + + this.globalEndpointManager = globalEndpointManager; + this.queryCompatibilityMode = queryCompatibilityMode; + + this.httpClient = httpClient; + } + + public Observable doCreate(RxDocumentServiceRequest request) { + return this.performRequest(request, HttpMethod.POST); + } + + public Observable doUpsert(RxDocumentServiceRequest request) { + return this.performRequest(request, HttpMethod.POST); + } + + public Observable doRead(RxDocumentServiceRequest request) { + return this.performRequest(request, HttpMethod.GET); + } + + public Observable doReplace(RxDocumentServiceRequest request) { + return this.performRequest(request, HttpMethod.PUT); + } + + public Observable doDelete(RxDocumentServiceRequest request) { + return this.performRequest(request, HttpMethod.DELETE); + } + + public Observable doExecute(RxDocumentServiceRequest request) { + return this.performRequest(request, HttpMethod.POST); + } + + public Observable doReadFeed(RxDocumentServiceRequest request) { + return this.performRequest(request, HttpMethod.GET); + } + + public Observable doQuery(RxDocumentServiceRequest request) { + request.getHeaders().put(HttpConstants.HttpHeaders.IS_QUERY, "true"); + + switch (this.queryCompatibilityMode) { + case SqlQuery: + request.getHeaders().put(HttpConstants.HttpHeaders.CONTENT_TYPE, + RuntimeConstants.MediaTypes.SQL); + break; + case Default: + case Query: + default: + request.getHeaders().put(HttpConstants.HttpHeaders.CONTENT_TYPE, + RuntimeConstants.MediaTypes.QUERY_JSON); + break; + } + return this.performRequest(request, HttpMethod.POST); + } + + /** + * Given the request it creates an observable which upon subscription issues HTTP call and emits one DocumentServiceResponse. + * + * @param request + * @param method + * @return Observable + */ + public Observable performRequest(RxDocumentServiceRequest request, HttpMethod method) { + + URI uri = getUri(request); + + HttpClientRequest httpRequest = HttpClientRequest.create(method, uri.toString()); + + this.fillHttpRequestBaseWithHeaders(request.getHeaders(), httpRequest); + try { + + if (request.getContentObservable() != null) { + + // TODO validate this + // convert byte[] to ByteBuf + // why not use Observable directly? + Observable byteBufObservable = request.getContentObservable() + .map(bytes -> Unpooled.wrappedBuffer(bytes)); + + httpRequest.withContentSource(byteBufObservable); + } else if (request.getContent() != null){ + httpRequest.withContent(request.getContent()); + } + + } catch (Exception e) { + return Observable.error(e); + } + + Observable> clientResponseObservable = this.httpClient.submit(httpRequest); + + return toDocumentServiceResponse(clientResponseObservable, request); + } + + private void fillHttpRequestBaseWithHeaders(Map headers, HttpClientRequest req) { + // Add default headers. + for (Map.Entry entry : this.defaultHeaders.entrySet()) { + req.withHeader(entry.getKey(), entry.getValue()); + } + // Add override headers. + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + req.withHeader(entry.getKey(), entry.getValue()); + } + } + } + + private URI getUri(RxDocumentServiceRequest request) { + URI rootUri = request.getEndpointOverride(); + if (rootUri == null) { + if (request.getIsMedia()) { + // For media read request, always use the write endpoint. + rootUri = this.globalEndpointManager.getWriteEndpoint(); + } else { + rootUri = this.globalEndpointManager.resolveServiceEndpoint(request.getOperationType()); + } + } + URI uri; + try { + uri = new URI("https", + null, + rootUri.getHost(), + rootUri.getPort(), + request.getPath(), + null, // Query string not used. + null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Incorrect uri from request.", e); + } + + return uri; + } + + private StoreResponse fromHttpResponse(HttpResponseStatus httpResponseStatus, + HttpResponseHeaders httpResponseHeaders, InputStream contentInputStream) throws IOException { + + List> headerEntries = httpResponseHeaders.entries(); + + String[] headers = new String[headerEntries.size()]; + String[] values = new String[headerEntries.size()]; + + int i = 0; + + for(Entry headerEntry: headerEntries) { + headers[i] = headerEntry.getKey(); + values[i] = headerEntry.getValue(); + i++; + } + + StoreResponse storeResponse = new StoreResponse( + headers, + values, + httpResponseStatus.code(), + contentInputStream); + + return storeResponse; + } + + private Observable toInputStream(Observable contentObservable) { + // TODO: this is a naive approach for converting to InputStream + // this first reads and buffers everything in memory and then translate that to an input stream + // this means + // 1) there is some performance implication + // 2) this may result in OutOfMemoryException if used for reading huge content, e.g., a media + // + // see this: https://github.com/ReactiveX/RxNetty/issues/391 for some similar discussion on how to + // convert to an input stream + return contentObservable + .reduce( + new ByteArrayOutputStream(), + (out, bb) -> { + try { + bb.readBytes(out, bb.readableBytes()); + return out; + } + catch (java.io.IOException e) { + throw new RuntimeException(e); + } + }) + .map(out -> { + return new ByteArrayInputStream(out.toByteArray()); + }); + } + + /** + * Transforms the rxNetty's client response Observable to DocumentServiceResponse Observable. + * + * + * Once the the customer code subscribes to the observable returned by the {@link AsyncDocumentClient} CRUD APIs, + * the subscription goes up till it reaches the source rxNetty's observable, and at that point the HTTP invocation will be made. + * + * @param clientResponseObservable + * @param request + * @return {@link Observable} + */ + private Observable toDocumentServiceResponse(Observable> clientResponseObservable, + RxDocumentServiceRequest request) { + + return clientResponseObservable.flatMap(clientResponse -> { + + // header key/value pairs + HttpResponseHeaders httpResponseHeaders = clientResponse.getHeaders(); + HttpResponseStatus httpResponseStatus = clientResponse.getStatus(); + + Observable inputStreamObservable; + + if (request.getOperationType() == OperationType.Delete) { + // for delete we don't expect any body + inputStreamObservable = Observable.just(null); + } else { + // transforms the observable to Observable + inputStreamObservable = toInputStream(clientResponse.getContent()); + } + + Observable storeResponseObservable = inputStreamObservable + .map(contentInputStream -> { + try { + // If there is any error in the header response this throws exception + validateOrThrow(request, httpResponseStatus, httpResponseHeaders, contentInputStream); + + // transforms to Observable + return fromHttpResponse(httpResponseStatus, httpResponseHeaders, contentInputStream); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + // wrap the exception in runtime exception + throw Exceptions.propagate(e); + } + }); + + return storeResponseObservable; + + }).map(storeResponse -> new DocumentServiceResponse(storeResponse)); + } + + private void validateOrThrow(RxDocumentServiceRequest request, HttpResponseStatus status, HttpResponseHeaders headers, + InputStream inputStream) throws DocumentClientException { + + int statusCode = status.code(); + + if (statusCode >= HttpConstants.StatusCodes.MINIMUM_STATUSCODE_AS_ERROR_GATEWAY) { + String body = null; + if (inputStream != null) { + try { + body = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + } catch (ParseException | IOException e) { + logger.error("Failed to get content from the http response", e); + throw new IllegalStateException("Failed to get content from the http response", e); + } finally { + IOUtils.closeQuietly(inputStream); + } + } + + Map responseHeaders = new HashMap(); + for (Entry header : headers.entries()) { + responseHeaders.put(header.getKey(), header.getValue()); + } + + String statusCodeString = status.reasonPhrase() != null + ? status.reasonPhrase().replace(" ", "") + : ""; + Error error = null; + error = (body != null)? new Error(body): new Error(); + error = new Error(statusCodeString, + String.format("%s, StatusCode: %s", error.getMessage(), statusCodeString), + error.getPartitionedQueryExecutionInfo()); + + throw new DocumentClientException(statusCode, error, responseHeaders); + } + } + + @Override + public Observable processMessage(RxDocumentServiceRequest request) { + switch (request.getOperationType()) { + case Create: + return this.doCreate(request); + case Upsert: + return this.doUpsert(request); + case Delete: + return this.doDelete(request); + case ExecuteJavaScript: + return this.doExecute(request); + case Read: + return this.doRead(request); + case ReadFeed: + return this.doReadFeed(request); + case Replace: + return this.doReplace(request); + case SqlQuery: + case Query: + return this.doQuery(request); + default: + throw new IllegalStateException("Unknown operation type " + request.getOperationType()); + } + } +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxRetryHandler.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxRetryHandler.java new file mode 100644 index 000000000000..82d103a47344 --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxRetryHandler.java @@ -0,0 +1,37 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + +import rx.Observable; + +interface RxRetryHandler { + + /** + * Returns an retry wait time observable or error if no retries must be made + * + * @param t + * @param attemptNumber + * @return + */ + Observable handleRetryAttempt(Throwable t, int attemptNumber); +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxStoreModel.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxStoreModel.java new file mode 100644 index 000000000000..7a9b49a33a0f --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxStoreModel.java @@ -0,0 +1,44 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.rx.internal.RxDocumentServiceRequest; +import com.microsoft.azure.documentdb.internal.DocumentServiceResponse; + +import rx.Observable; + +interface RxStoreModel { + + /** + * Given the request, it returns an Observable of the response. + * + * The Observable upon subscription will execute the request and upon successful execution request returns a single {@link DocumentServiceResponse}. + * If the execution of the request fails it returns an error. + * + * @param request + * @return + * @throws DocumentClientException + */ + Observable processMessage(RxDocumentServiceRequest request) throws DocumentClientException; +} diff --git a/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxWrapperDocumentClientImpl.java b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxWrapperDocumentClientImpl.java new file mode 100644 index 000000000000..c4cc9258749f --- /dev/null +++ b/azure-documentdb-rx/src/main/java/com/microsoft/azure/documentdb/rx/internal/RxWrapperDocumentClientImpl.java @@ -0,0 +1,1202 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.microsoft.azure.documentdb.Attachment; +import com.microsoft.azure.documentdb.Conflict; +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.DatabaseAccount; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClient; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.FeedOptions; +import com.microsoft.azure.documentdb.FeedResponse; +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.MediaOptions; +import com.microsoft.azure.documentdb.MediaResponse; +import com.microsoft.azure.documentdb.Offer; +import com.microsoft.azure.documentdb.Permission; +import com.microsoft.azure.documentdb.QueryIterable; +import com.microsoft.azure.documentdb.RequestOptions; +import com.microsoft.azure.documentdb.Resource; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.SqlQuerySpec; +import com.microsoft.azure.documentdb.StoredProcedure; +import com.microsoft.azure.documentdb.StoredProcedureResponse; +import com.microsoft.azure.documentdb.Trigger; +import com.microsoft.azure.documentdb.User; +import com.microsoft.azure.documentdb.UserDefinedFunction; +import com.microsoft.azure.documentdb.internal.HttpConstants; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Scheduler; +import rx.Subscriber; +import rx.functions.Func0; +import rx.internal.util.RxThreadFactory; +import rx.schedulers.Schedulers; + +/** + * This class provides a RX wrapper for existing blocking io operation, and is meant to be used internally. + * For query APIs and a few other things we use this class, in the long term, after we implement all + * APIs as non-blocking top to bottom, this will will be removed. + * + */ +public class RxWrapperDocumentClientImpl implements AsyncDocumentClient { + + private final Logger logger = LoggerFactory.getLogger(AsyncDocumentClient.class); + private final DocumentClient client; + private final Scheduler scheduler; + private final ExecutorService executorService; + + public RxWrapperDocumentClientImpl(DocumentClient client) { + this.client = client; + + int maxThreads = (int) (client.getConnectionPolicy().getMaxPoolSize() * 1.1); + this.executorService = new ThreadPoolExecutor( + Math.min(8, maxThreads), // core thread pool size + maxThreads, // maximum thread pool size + 30, // time to wait before killing idle threads + TimeUnit.SECONDS, + new SynchronousQueue<>(), + new RxThreadFactory("RxDocdb-io"), + new ThreadPoolExecutor.CallerRunsPolicy()); + this.scheduler = Schedulers.from(executorService); + } + + @Override + public URI getServiceEndpoint() { + return client.getServiceEndpoint(); + } + + @Override + public URI getWriteEndpoint() { + return client.getWriteEndpoint(); + } + + @Override + public URI getReadEndpoint() { + return client.getReadEndpoint(); + } + + @Override + public ConnectionPolicy getConnectionPolicy() { + return client.getConnectionPolicy(); + } + + private interface ImplFunc { + T invoke() throws Exception; + } + + private Observable createDeferObservable(final ImplFunc impl) { + return Observable.defer(new Func0>() { + + @Override + public Observable call() { + + try { + T rr = impl.invoke(); + return Observable.just(rr); + } catch (Exception e) { + return Observable.error(e); + } + } + }).subscribeOn(scheduler); + } + + private Observable> createResourceResponseObservable( + final ImplFunc> impl) { + logger.trace("Creating Observable>"); + return Observable.defer(new Func0>>() { + + @Override + public Observable> call() { + + try { + ResourceResponse rr = impl.invoke(); + return Observable.just(rr); + } catch (Exception e) { + return Observable.error(flatten(e)); + } + } + }).subscribeOn(scheduler); + } + + private int getHeaderItemCount(Map header) { + int pageSize = -1; + try { + String pageSizeHeaderValue = header!= null?header.get(HttpConstants.HttpHeaders.ITEM_COUNT):null; + if (pageSizeHeaderValue != null) { + pageSize = Integer.valueOf(pageSizeHeaderValue); + } else { + logger.debug("Page Item Count header is missing"); + pageSize = -1; + } + + } catch (Exception e) { + logger.debug("Page Item Count header is missing", e); + } + return pageSize; + } + + private Observable> createFeedResponsePageObservable(final ImplFunc> impl) { + + OnSubscribe> obs = new OnSubscribe>() { + @Override + public void call(Subscriber> subscriber) { + + try { + FeedResponse feedResponse = impl.invoke(); + + final QueryIterable qi = feedResponse.getQueryIterable(); + + int numberOfPages = 0; + while (!subscriber.isUnsubscribed()) { + Map header = feedResponse.getResponseHeaders(); + logger.trace("Page Header key/value map {}", header); + int pageSize = getHeaderItemCount(header); + + List pageResults = qi.fetchNextBlock(); + if (pageResults == null) { + pageResults = new ArrayList<>(); + } + + logger.trace("Found [{}] results in page with Header key/value map [{}]", pageResults.size(), header); + + if (pageResults.size() != pageSize) { + logger.trace("Actual pageSize [{}] must match header page Size [{}] But it doesn't", pageResults.size(), pageSize); + } + + if (pageResults.isEmpty() && feedResponse.getResponseContinuation() == null) { + // finished + break; + } + + FeedResponsePage frp = + new FeedResponsePage(pageResults, feedResponse.getResponseHeaders()); + subscriber.onNext(frp); + numberOfPages++; + } + + if (!subscriber.isUnsubscribed() && numberOfPages == 0) { + // if no results, return one single feed response page containing the response headers + subscriber.onNext(new FeedResponsePage<>(new ArrayList<>(), feedResponse.getResponseHeaders())); + } + } catch (Exception e) { + logger.debug("Query Failed due to [{}]", e.getMessage(), e); + if (!subscriber.isUnsubscribed()) { + subscriber.onError(flatten(e)); + } + return; + } + if (!subscriber.isUnsubscribed()) { + subscriber.onCompleted(); + } + return; + } + }; + return Observable.defer(() -> Observable.create(obs)).subscribeOn(scheduler); + } + + private Throwable flatten(Throwable e) { + while (e instanceof RuntimeException && e.getCause() != null) { + e = e.getCause(); + } + return e; + } + + @Override + public Observable> createDatabase(final Database database, final RequestOptions options) { + + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createDatabase(database, options); + } + }); + } + + @Override + public Observable> deleteDatabase(final String databaseLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deleteDatabase(databaseLink, options); + } + }); + } + + @Override + public Observable> readDatabase(final String databaseLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readDatabase(databaseLink, options); + } + }); + } + + @Override + public Observable> readDatabases(final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readDatabases(options); + } + }); + } + + @Override + public Observable> queryDatabases(final String query, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryDatabases(query, options); + } + }); + } + + @Override + public Observable> queryDatabases(final SqlQuerySpec querySpec, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryDatabases(querySpec, options); + } + }); + } + + @Override + public Observable> createCollection(final String databaseLink, + final DocumentCollection collection, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createCollection(databaseLink, collection, options); + } + }); + } + + @Override + public Observable> replaceCollection(final DocumentCollection collection, + final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replaceCollection(collection, options); + } + }); + } + + @Override + public Observable> deleteCollection(final String collectionLink, + final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deleteCollection(collectionLink, options); + } + }); + } + + @Override + public Observable> readCollection(final String collectionLink, + final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readCollection(collectionLink, options); + } + }); + } + + @Override + public Observable> readCollections(final String databaseLink, + final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readCollections(databaseLink, options); + } + }); + } + + @Override + public Observable> queryCollections(final String databaseLink, final String query, + final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryCollections(databaseLink, query, options); + } + }); + } + + @Override + public Observable> queryCollections(final String databaseLink, final + SqlQuerySpec querySpec, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryCollections(databaseLink, querySpec, options); + } + }); + } + + @Override + public Observable> createDocument(final String collectionLink, final Object document, final + RequestOptions options, final boolean disableAutomaticIdGeneration) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createDocument(collectionLink, document, options, disableAutomaticIdGeneration); + } + }); + } + + @Override + public Observable> upsertDocument(final String collectionLink, final Object document, final + RequestOptions options, final boolean disableAutomaticIdGeneration) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.upsertDocument(collectionLink, document, options, disableAutomaticIdGeneration); + } + }); + } + + @Override + public Observable> replaceDocument(final String documentLink, final Object document, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replaceDocument(documentLink, document, options); + } + }); + } + + @Override + public Observable> replaceDocument(final Document document, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replaceDocument(document, options); + } + }); + } + + @Override + public Observable> deleteDocument(final String documentLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deleteDocument(documentLink, options); + } + }); + } + + @Override + public Observable> readDocument(final String documentLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readDocument(documentLink, options); + } + }); + } + + @Override + public Observable> readDocuments(final String collectionLink, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readDocuments(collectionLink, options); + } + }); + } + + @Override + public Observable> queryDocuments(final String collectionLink, final String query, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryDocuments(collectionLink, query, options); + } + }); + } + + @Override + public Observable> queryDocuments(final String collectionLink, final String query, final + FeedOptions options, final Object partitionKey) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryDocuments(collectionLink, query, options, partitionKey); + } + }); + } + + @Override + public Observable> queryDocuments(final String collectionLink, final SqlQuerySpec querySpec, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryDocuments(collectionLink, querySpec, options); + } + }); + } + + @Override + public Observable> queryDocuments(final String collectionLink, final SqlQuerySpec querySpec, final + FeedOptions options, final Object partitionKey) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryDocuments(collectionLink, querySpec, options, partitionKey); + } + }); + } + + @Override + public Observable> createStoredProcedure(final String collectionLink, final + StoredProcedure storedProcedure, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createStoredProcedure(collectionLink, storedProcedure, options); + } + }); + } + + @Override + public Observable> upsertStoredProcedure(final String collectionLink, final + StoredProcedure storedProcedure, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.upsertStoredProcedure(collectionLink, storedProcedure, options); + } + }); + } + + @Override + public Observable> replaceStoredProcedure(final StoredProcedure storedProcedure, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replaceStoredProcedure(storedProcedure, options); + } + }); + } + + @Override + public Observable> deleteStoredProcedure(final String storedProcedureLink, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deleteStoredProcedure(storedProcedureLink, options); + } + }); + } + + @Override + public Observable> readStoredProcedure(final String storedProcedureLink, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readStoredProcedure(storedProcedureLink, options); + } + }); + } + + @Override + public Observable> readStoredProcedures(final String collectionLink, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readStoredProcedures(collectionLink, options); + } + }); + } + + @Override + public Observable> queryStoredProcedures(final String collectionLink, final String query, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryStoredProcedures(collectionLink, query, options); + } + }); + } + + @Override + public Observable> queryStoredProcedures(final String collectionLink, final + SqlQuerySpec querySpec, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryStoredProcedures(collectionLink, querySpec, options); + } + }); + } + + @Override + public Observable executeStoredProcedure(final String storedProcedureLink, final Object[] procedureParams) { + return this.createDeferObservable(new ImplFunc() { + @Override + public StoredProcedureResponse invoke() throws Exception { + return client.executeStoredProcedure(storedProcedureLink, procedureParams); + } + }); + } + + @Override + public Observable executeStoredProcedure(final String storedProcedureLink, final RequestOptions options, final + Object[] procedureParams) { + return this.createDeferObservable(new ImplFunc() { + @Override + public StoredProcedureResponse invoke() throws Exception { + return client.executeStoredProcedure(storedProcedureLink, options, procedureParams); + } + }); + } + + @Override + public Observable> createTrigger(final String collectionLink, final Trigger trigger, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createTrigger(collectionLink, trigger, options); + } + }); + } + + @Override + public Observable> upsertTrigger(final String collectionLink, final Trigger trigger, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createTrigger(collectionLink, trigger, options); + } + }); + } + + @Override + public Observable> replaceTrigger(final Trigger trigger, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replaceTrigger(trigger, options); + } + }); + } + + @Override + public Observable> deleteTrigger(final String triggerLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deleteTrigger(triggerLink, options); + } + }); + } + + @Override + public Observable> readTrigger(final String triggerLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readTrigger(triggerLink, options); + } + }); + } + + @Override + public Observable> readTriggers(final String collectionLink, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readTriggers(collectionLink, options); + } + }); + } + + @Override + public Observable> queryTriggers(final String collectionLink, final String query, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryTriggers(collectionLink, query, options); + } + }); + } + + @Override + public Observable> queryTriggers(final String collectionLink, final SqlQuerySpec querySpec, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryTriggers(collectionLink, querySpec, options); + } + }); + } + + @Override + public Observable> createUserDefinedFunction(final String collectionLink, final + UserDefinedFunction udf, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createUserDefinedFunction(collectionLink, udf, options); + } + }); + } + + @Override + public Observable> upsertUserDefinedFunction(final String collectionLink, final + UserDefinedFunction udf, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.upsertUserDefinedFunction(collectionLink, udf, options); + } + }); + } + + @Override + public Observable> replaceUserDefinedFunction(final UserDefinedFunction udf, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replaceUserDefinedFunction(udf, options); + } + }); + } + + @Override + public Observable> deleteUserDefinedFunction(final String udfLink, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deleteUserDefinedFunction(udfLink, options); + } + }); + } + + @Override + public Observable> readUserDefinedFunction(final String udfLink, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readUserDefinedFunction(udfLink, options); + } + }); + } + + @Override + public Observable> readUserDefinedFunctions(final String collectionLink, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readUserDefinedFunctions(collectionLink, options); + } + }); + } + + @Override + public Observable> queryUserDefinedFunctions(final String collectionLink, final + String query, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryUserDefinedFunctions(collectionLink, query, options); + } + }); + } + + @Override + public Observable> queryUserDefinedFunctions(final String collectionLink, final + SqlQuerySpec querySpec, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryUserDefinedFunctions(collectionLink, querySpec, options); + } + }); + } + + @Override + public Observable> createAttachment(final String documentLink, final Attachment attachment, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createAttachment(documentLink, attachment , options); + } + }); + } + + @Override + public Observable> upsertAttachment(final String documentLink, final Attachment attachment, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.upsertAttachment(documentLink, attachment , options); + } + }); + } + + @Override + public Observable> replaceAttachment(final Attachment attachment, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replaceAttachment(attachment , options); + } + }); + } + + @Override + public Observable> deleteAttachment(final String attachmentLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deleteAttachment(attachmentLink , options); + } + }); + } + + @Override + public Observable> readAttachment(final String attachmentLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readAttachment(attachmentLink , options); + } + }); + } + + @Override + public Observable> readAttachments(final String documentLink, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readAttachments(documentLink, options); + } + }); + } + + @Override + public Observable> queryAttachments(final String documentLink, final String query, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryAttachments(documentLink, query, options); + } + }); + } + + @Override + public Observable> queryAttachments(final String documentLink, final SqlQuerySpec querySpec, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryAttachments(documentLink, querySpec, options); + } + }); + } + + @Override + public Observable> createAttachment(final String documentLink, final InputStream mediaStream, final + MediaOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createAttachment(documentLink, mediaStream, options); + } + }); + } + + @Override + public Observable> upsertAttachment(final String documentLink, final InputStream mediaStream, final + MediaOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.upsertAttachment(documentLink, mediaStream, options); + } + }); + } + + @Override + public Observable readMedia(final String mediaLink) { + return this.createDeferObservable(new ImplFunc() { + @Override + public MediaResponse invoke() throws Exception { + return client.readMedia(mediaLink); + } + }); + } + + @Override + public Observable updateMedia(final String mediaLink, final InputStream mediaStream, final MediaOptions options) { + return this.createDeferObservable(new ImplFunc() { + @Override + public MediaResponse invoke() throws Exception { + return client.updateMedia(mediaLink, mediaStream, options); + } + }); + } + + @Override + public Observable> readConflict(final String conflictLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readConflict(conflictLink, options); + } + }); + } + + @Override + public Observable> readConflicts(final String collectionLink, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readConflicts(collectionLink, options); + } + }); + } + + @Override + public Observable> queryConflicts(final String collectionLink, final String query, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryConflicts(collectionLink, query, options); + } + }); + } + + @Override + public Observable> queryConflicts(final String collectionLink, final SqlQuerySpec querySpec, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryConflicts(collectionLink, querySpec, options); + } + }); + } + + @Override + public Observable> deleteConflict(final String conflictLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deleteConflict(conflictLink, options); + } + }); + } + + @Override + public Observable> createUser(final String databaseLink, final User user, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createUser(databaseLink, user, options); + } + }); + } + + @Override + public Observable> upsertUser(final String databaseLink, final User user, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.upsertUser(databaseLink, user, options); + } + }); + } + + @Override + public Observable> replaceUser(final User user, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replaceUser(user, options); + } + }); + } + + @Override + public Observable> deleteUser(final String userLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deleteUser(userLink, options); + } + }); + } + + @Override + public Observable> readUser(final String userLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readUser(userLink, options); + } + }); + } + + @Override + public Observable> readUsers(final String databaseLink, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readUsers(databaseLink, options); + } + }); + } + + @Override + public Observable> queryUsers(final String databaseLink, final String query, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryUsers(databaseLink, query, options); + } + }); + } + + @Override + public Observable> queryUsers(final String databaseLink, final SqlQuerySpec querySpec, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryUsers(databaseLink, querySpec, options); + } + }); + } + + @Override + public Observable> createPermission(final String userLink, final Permission permission, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.createPermission(userLink, permission, options); + } + }); + } + + @Override + public Observable> upsertPermission(final String userLink, final Permission permission, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.upsertPermission(userLink, permission, options); + } + }); + } + + @Override + public Observable> replacePermission(final Permission permission, final + RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replacePermission(permission, options); + } + }); + } + + @Override + public Observable> deletePermission(final String permissionLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.deletePermission(permissionLink, options); + } + }); + } + + @Override + public Observable> readPermission(final String permissionLink, final RequestOptions options) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readPermission(permissionLink, options); + } + }); + } + + @Override + public Observable> readPermissions(final String permissionLink, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readPermissions(permissionLink, options); + } + }); + } + + @Override + public Observable> queryPermissions(final String permissionLink, final String query, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryPermissions(permissionLink, query, options); + } + }); + } + + @Override + public Observable> queryPermissions(final String permissionLink, final SqlQuerySpec querySpec, final + FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryPermissions(permissionLink, querySpec, options); + } + }); + } + + @Override + public Observable> replaceOffer(final Offer offer) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.replaceOffer(offer); + } + }); + } + + @Override + public Observable> readOffer(final String offerLink) { + return this.createResourceResponseObservable(new ImplFunc>() { + @Override + public ResourceResponse invoke() throws Exception { + return client.readOffer(offerLink); + } + }); + } + + @Override + public Observable> readOffers(final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.readOffers(options); + } + }); + } + + @Override + public Observable> queryOffers(final String query, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryOffers(query, options); + } + }); + } + + @Override + public Observable> queryOffers(final SqlQuerySpec querySpec, final FeedOptions options) { + return this.createFeedResponsePageObservable(new ImplFunc>() { + @Override + public FeedResponse invoke() throws Exception { + return client.queryOffers(querySpec, options); + } + }); + } + + @Override + public Observable getDatabaseAccount() { + return this.createDeferObservable(new ImplFunc() { + @Override + public DatabaseAccount invoke() throws Exception { + return client.getDatabaseAccount(); + } + }); + } + + private void safeShutdownExecutorService(ExecutorService exS) { + if (exS == null) { + return; + } + + try { + exS.shutdown(); + exS.awaitTermination(15, TimeUnit.SECONDS); + } catch (Exception e) { + logger.warn("Failure in shutting down a executor service", e); + } + } + + @Override + public void close() { + safeShutdownExecutorService(this.executorService); + client.close(); + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/CollectionCrudTest.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/CollectionCrudTest.java new file mode 100644 index 000000000000..52f523c831a5 --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/CollectionCrudTest.java @@ -0,0 +1,166 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.IndexingMode; +import com.microsoft.azure.documentdb.IndexingPolicy; +import com.microsoft.azure.documentdb.ResourceResponse; + +import rx.Observable; + +public class CollectionCrudTest extends TestSuiteBase { + private final static String DATABASE_ID = getDatabaseId(CollectionCrudTest.class); + + private AsyncDocumentClient client; + private Database database; + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createCollection() throws Exception { + DocumentCollection collectionDefinition = getCollectionDefinition(); + + Observable> createObservable = client + .createCollection(database.getSelfLink(), collectionDefinition, null); + + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(collectionDefinition.getId()).build(); + + validateSuccess(createObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void readCollection() throws Exception { + DocumentCollection collectionDefinition = getCollectionDefinition(); + + Observable> createObservable = client.createCollection(database.getSelfLink(), collectionDefinition, + null); + DocumentCollection collection = createObservable.toBlocking().single().getResource(); + + Observable> readObservable = client.readCollection(collection.getSelfLink(), null); + + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(collection.getId()).build(); + validateSuccess(readObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void readCollection_NameBase() throws Exception { + DocumentCollection collectionDefinition = getCollectionDefinition(); + + Observable> createObservable = client.createCollection(database.getSelfLink(), collectionDefinition, + null); + DocumentCollection collection = createObservable.toBlocking().single().getResource(); + + Observable> readObservable = client.readCollection( + Utils.getCollectionNameLink(database.getId(), collection.getId()), null); + + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(collection.getId()).build(); + validateSuccess(readObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void readCollection_DoesntExist() throws Exception { + + Observable> readObservable = client + .readCollection(Utils.getCollectionNameLink(database.getId(), "I don't exist"), null); + + FailureValidator validator = new FailureValidator.Builder().resourceNotFound().build(); + validateFailure(readObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void deleteCollection() throws Exception { + DocumentCollection collectionDefinition = getCollectionDefinition(); + + Observable> createObservable = client.createCollection(database.getSelfLink(), collectionDefinition, null); + DocumentCollection collection = createObservable.toBlocking().single().getResource(); + + Observable> deleteObservable = client.deleteCollection(collection.getSelfLink(), + null); + + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .nullResource().build(); + validateSuccess(deleteObservable, validator); + + //TODO validate after deletion the resource is actually deleted (not found) + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void replaceCollection() throws Exception { + // create a collection + DocumentCollection collectionDefinition = getCollectionDefinition(); + Observable> createObservable = client.createCollection(database.getSelfLink(), collectionDefinition, null); + DocumentCollection collection = createObservable.toBlocking().single().getResource(); + // sanity check + assertThat(collection.getIndexingPolicy().getIndexingMode()).isEqualTo(IndexingMode.Consistent); + + // replace indexing mode + IndexingPolicy indexingMode = new IndexingPolicy(); + indexingMode.setIndexingMode(IndexingMode.Lazy); + collection.setIndexingPolicy(indexingMode); + Observable> readObservable = client.replaceCollection(collection, null); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .indexingMode(IndexingMode.Lazy).build(); + validateSuccess(readObservable, validator); + } + + @BeforeClass(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeClass() { + // set up the client + + client = new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(ConnectionPolicy.GetDefault()) + .withConsistencyLevel(ConsistencyLevel.Session).build(); + + Database databaseDefinition = new Database(); + databaseDefinition.setId(DATABASE_ID); + + try { + client.deleteDatabase(Utils.getDatabaseLink(databaseDefinition, true), null).toBlocking().single(); + } catch (Exception e) { + // ignore failure if it doesn't exist + } + + database = client.createDatabase(databaseDefinition, null).toBlocking().single().getResource(); + } + + @AfterClass(groups = { "simple" }, timeOut = SHUTDOWN_TIMEOUT) + public void afterClass() { + client.deleteDatabase(database.getSelfLink(), null).toBlocking().single(); + client.close(); + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/DatabaseCrudTest.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/DatabaseCrudTest.java new file mode 100644 index 000000000000..bf36f7e75dea --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/DatabaseCrudTest.java @@ -0,0 +1,151 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; + +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient.Builder; + +import rx.Observable; + +public class DatabaseCrudTest extends TestSuiteBase { + private final static String PRE_EXISTING_DATABASE_ID = getDatabaseId(DatabaseCrudTest.class) + "1"; + private final static String DATABASE_ID2 = getDatabaseId(DatabaseCrudTest.class) + "2"; + + private AsyncDocumentClient client; + private Builder clientBuilder; + + @Factory(dataProvider = "clientBuilders") + public DatabaseCrudTest(AsyncDocumentClient.Builder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createDatabase() throws Exception { + Database databaseDefinition = new Database(); + databaseDefinition.setId(DATABASE_ID2); + + // create the database + Observable> createObservable = client.createDatabase(databaseDefinition, null); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(databaseDefinition.getId()).build(); + validateSuccess(createObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createDatabase_AlreadyExists() throws Exception { + Database databaseDefinition = new Database(); + databaseDefinition.setId(DATABASE_ID2); + + client.createDatabase(databaseDefinition, null).toBlocking().single(); + + // attempt to create the database + Observable> createObservable = client.createDatabase(databaseDefinition, null); + + // validate + FailureValidator validator = new FailureValidator.Builder().resourceAlreadyExists().build(); + validateFailure(createObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void readDatabase() throws Exception { + // read database + Observable> readObservable = client + .readDatabase(Utils.getDatabaseNameLink(PRE_EXISTING_DATABASE_ID), null); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(PRE_EXISTING_DATABASE_ID).build(); + validateSuccess(readObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void readDatabase_DoesntExist() throws Exception { + // read database + Observable> readObservable = client + .readDatabase(Utils.getDatabaseNameLink("I don't exist"), null); + + // validate + FailureValidator validator = new FailureValidator.Builder().resourceNotFound().build(); + validateFailure(readObservable, validator); + } + + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void deleteDatabase() throws Exception { + // delete the database + Observable> deleteObservable = client + .deleteDatabase(Utils.getDatabaseNameLink(PRE_EXISTING_DATABASE_ID), null); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .nullResource().build(); + validateSuccess(deleteObservable, validator); + //TODO validate after deletion the resource is actually deleted (not found) + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void deleteDatabase_DoesntExist() throws Exception { + // delete the database + Observable> deleteObservable = client + .deleteDatabase(Utils.getDatabaseNameLink("I don't exist"), null); + + // validate + FailureValidator validator = new FailureValidator.Builder().resourceNotFound().build(); + validateFailure(deleteObservable, validator); + } + + @BeforeClass(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeClass() { + client = clientBuilder.build(); + } + + @AfterClass(groups = { "simple" }, timeOut = SHUTDOWN_TIMEOUT) + public void afterClass() { + client.close(); + } + + @AfterMethod(groups = { "simple" }, timeOut = SHUTDOWN_TIMEOUT) + public void afterMethod() { + try { + deleteDatabase(client, PRE_EXISTING_DATABASE_ID); + } catch (Exception e) {} + try { + deleteDatabase(client, DATABASE_ID2); + } catch (Exception e) {} + } + + @BeforeMethod(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeMethod() { + createDatabase(client, PRE_EXISTING_DATABASE_ID); + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/DocumentCrudTest.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/DocumentCrudTest.java new file mode 100644 index 000000000000..a26c0043e183 --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/DocumentCrudTest.java @@ -0,0 +1,274 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterSuite; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; + +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.PartitionKey; +import com.microsoft.azure.documentdb.RequestOptions; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient.Builder; + +import rx.Observable; + +public class DocumentCrudTest extends TestSuiteBase { + + public final static String DATABASE_ID = getDatabaseId(DocumentCrudTest.class); + + private static AsyncDocumentClient houseKeepingClient; + private static Database createdDatabase; + private static DocumentCollection createdCollection; + + private Builder clientBuilder; + private AsyncDocumentClient client; + + @Factory(dataProvider = "clientBuilders") + public DocumentCrudTest(AsyncDocumentClient.Builder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createDocument() throws Exception { + Document docDefinition = getDocumentDefinition(); + + Observable> createObservable = client + .createDocument(getCollectionLink(), docDefinition, null, false); + + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(docDefinition.getId()) + .build(); + + validateSuccess(createObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createDocument_AlreadyExists() throws Exception { + Document docDefinition = getDocumentDefinition(); + + client.createDocument(getCollectionLink(), docDefinition, null, false).toBlocking().single().getResource(); + + Observable> createObservable = client + .createDocument(getCollectionLink(), docDefinition, null, false); + + FailureValidator validator = new FailureValidator.Builder().resourceAlreadyExists().build(); + validateFailure(createObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createDocumentTimeout() throws Exception { + Document docDefinition = getDocumentDefinition(); + + Observable> createObservable = client + .createDocument(getCollectionLink(), docDefinition, null, false) + .timeout(1, TimeUnit.MILLISECONDS); + + FailureValidator validator = new FailureValidator.Builder().instanceOf(TimeoutException.class).build(); + + validateFailure(createObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void readDocument() throws Exception { + Document docDefinition = getDocumentDefinition(); + + Document document = client + .createDocument(getCollectionLink(), docDefinition, null, false).toBlocking().single().getResource(); + + + RequestOptions options = new RequestOptions(); + options.setPartitionKey(new PartitionKey(document.get("mypk"))); + Observable> readObservable = client.readDocument(document.getSelfLink(), options); + + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(document.getId()) + .build(); + validateSuccess(readObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void readDocument_DoesntExist() throws Exception { + Document docDefinition = getDocumentDefinition(); + + Document document = client + .createDocument(getCollectionLink(), docDefinition, null, false).toBlocking().single().getResource(); + + RequestOptions options = new RequestOptions(); + options.setPartitionKey(new PartitionKey(document.get("mypk"))); + client.deleteDocument(document.getSelfLink(), options).toBlocking().first(); + + options.setPartitionKey(new PartitionKey("looloo")); + Observable> readObservable = client.readDocument(document.getSelfLink(), options); + + FailureValidator validator = new FailureValidator.Builder().instanceOf(DocumentClientException.class) + .statusCode(404).build(); + validateFailure(readObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void deleteDocument() throws Exception { + Document docDefinition = getDocumentDefinition(); + + Document document = client + .createDocument(getCollectionLink(), docDefinition, null, false).toBlocking().single().getResource(); + + RequestOptions options = new RequestOptions(); + options.setPartitionKey(new PartitionKey(document.get("mypk"))); + Observable> deleteObservable = client.deleteDocument(document.getSelfLink(), options); + + + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .nullResource().build(); + validateSuccess(deleteObservable, validator); + + //TODO validate after deletion the resource is actually deleted (not found) + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void deleteDocument_DoesntExist() throws Exception { + Document docDefinition = getDocumentDefinition(); + + Document document = client + .createDocument(getCollectionLink(), docDefinition, null, false).toBlocking().single().getResource(); + + RequestOptions options = new RequestOptions(); + options.setPartitionKey(new PartitionKey(document.get("mypk"))); + client.deleteDocument(document.getSelfLink(), options).toBlocking().single(); + + // delete again + Observable> deleteObservable = client.deleteDocument(document.getSelfLink(), options); + + FailureValidator validator = new FailureValidator.Builder().resourceNotFound().build(); + validateFailure(deleteObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void replaceDocument() throws Exception { + // create a document + Document docDefinition = getDocumentDefinition(); + + Document document = client + .createDocument(getCollectionLink(), docDefinition, null, false).toBlocking().single().getResource(); + + String newPropValue = UUID.randomUUID().toString(); + document.set("newProp", newPropValue); + + // replace document + Observable> readObservable = client.replaceDocument(document, null); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withProperty("newProp", newPropValue).build(); + validateSuccess(readObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void upsertDocument_CreateDocument() throws Exception { + // create a document + Document docDefinition = getDocumentDefinition(); + + + // replace document + Observable> upsertObservable = client.upsertDocument(getCollectionLink(), + docDefinition, null, false); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(docDefinition.getId()).build(); + validateSuccess(upsertObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void upsertDocument_ReplaceDocument() throws Exception { + // create a document + Document docDefinition = getDocumentDefinition(); + + Document document = client + .createDocument(getCollectionLink(), docDefinition, null, false).toBlocking().single().getResource(); + + String newPropValue = UUID.randomUUID().toString(); + document.set("newProp", newPropValue); + + // replace document + Observable> readObservable = client.upsertDocument + (getCollectionLink(), document, null, true); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withProperty("newProp", newPropValue).build(); + validateSuccess(readObservable, validator); + } + + @BeforeSuite(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public static void beforeSuite() { + houseKeepingClient = createGatewayRxDocumentClient().build(); + Database d = new Database(); + d.setId(DATABASE_ID); + createdDatabase = safeCreateDatabase(houseKeepingClient, d); + createdCollection = safeCreateCollection(houseKeepingClient, createdDatabase.getSelfLink(), getCollectionDefinition()); + } + + @AfterSuite(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public static void afterSuite() { + + deleteDatabase(houseKeepingClient, createdDatabase.getId()); + houseKeepingClient.close(); + } + + private String getCollectionLink() { + return createdCollection.getSelfLink(); + } + + @BeforeClass(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeClass() { + this.client = this.clientBuilder.build(); + } + + @AfterClass(groups = { "simple" }, timeOut = SHUTDOWN_TIMEOUT) + public void afterClass() { + this.client.close(); + } + + private Document getDocumentDefinition() { + String uuid = UUID.randomUUID().toString(); + Document doc = new Document(String.format("{ " + + "\"id\": \"%s\", " + + "\"mypk\": \"%s\", " + + "\"sgmts\": [[6519456, 1471916863], [2498434, 1455671440]]" + + "}" + , uuid, uuid)); + return doc; + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/DocumentQueryTest.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/DocumentQueryTest.java new file mode 100644 index 000000000000..7f37ea1654fa --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/DocumentQueryTest.java @@ -0,0 +1,252 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.testng.annotations.AfterSuite; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; + +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.FeedOptions; +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.PartitionKeyDefinition; +import com.microsoft.azure.documentdb.RequestOptions; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient.Builder; + +import rx.Observable; + +public class DocumentQueryTest extends TestSuiteBase { + + public final static String DATABASE_ID = getDatabaseId(DocumentQueryTest.class); + + private static Database createdDatabase; + private static DocumentCollection createdCollection; + private static AsyncDocumentClient houseKeepingClient; + private static List createdDocuments = new ArrayList<>(); + + private Builder clientBuilder; + private AsyncDocumentClient client; + + public static String getCollectionLink() { + return createdCollection.getSelfLink(); + } + + static protected DocumentCollection getCollectionDefinition() { + PartitionKeyDefinition partitionKeyDef = new PartitionKeyDefinition(); + ArrayList paths = new ArrayList(); + paths.add("/mypk"); + partitionKeyDef.setPaths(paths); + + RequestOptions options = new RequestOptions(); + options.setOfferThroughput(10100); + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setId(UUID.randomUUID().toString()); + collectionDefinition.setPartitionKey(partitionKeyDef); + + return collectionDefinition; + } + + @Factory(dataProvider = "clientBuilders") + public DocumentQueryTest(AsyncDocumentClient.Builder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void queryDocuments() throws Exception { + + String query = "SELECT * from root"; + FeedOptions options = new FeedOptions(); + options.setEnableCrossPartitionQuery(true); + Observable> queryObservable = client + .queryDocuments(getCollectionLink(), query, options); + + FeedResponsePageListValidator validator = new FeedResponsePageListValidator + .Builder() + .containsExactly(createdDocuments + .stream() + .map(d -> d.getResourceId()) + .collect(Collectors.toList())) + .allPagesSatisfy(new FeedResponsePageValidator.Builder() + .requestChargeGreaterThanOrEqualTo(1.0).build()) + .build(); + validateQuerySuccess(queryObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void queryDocuments_NoResults() throws Exception { + + String query = "SELECT * from root r where r.id = '2'"; + FeedOptions options = new FeedOptions(); + options.setEnableCrossPartitionQuery(true); + Observable> queryObservable = client + .queryDocuments(getCollectionLink(), query, options); + + FeedResponsePageListValidator validator = new FeedResponsePageListValidator.Builder() + .containsExactly(new ArrayList<>()) + .numberOfPages(1) + .pageSatisfy(0, new FeedResponsePageValidator.Builder() + .requestChargeGreaterThanOrEqualTo(1.0).build()) + .build(); + validateQuerySuccess(queryObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void queryDocumentsWithPageSize() throws Exception { + + String query = "SELECT * from root"; + FeedOptions options = new FeedOptions(); + options.setPageSize(3); + options.setEnableCrossPartitionQuery(true); + Observable> queryObservable = client + .queryDocuments(getCollectionLink(), query, options); + + FeedResponsePageListValidator validator = new FeedResponsePageListValidator + .Builder() + .containsExactly(createdDocuments + .stream() + .map(d -> d.getResourceId()) + .collect(Collectors.toList())) + .numberOfPages((createdDocuments.size() + 1) / 3) + .allPagesSatisfy(new FeedResponsePageValidator.Builder() + .requestChargeGreaterThanOrEqualTo(1.0).build()) + .build(); + validateQuerySuccess(queryObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void queryOrderBy() throws Exception { + + String query = "SELECT * FROM r ORDER BY r.prop ASC"; + FeedOptions options = new FeedOptions(); + options.setEnableCrossPartitionQuery(true); + options.setPageSize(3); + Observable> queryObservable = client + .queryDocuments(getCollectionLink(), query, options); + + FeedResponsePageListValidator validator = new FeedResponsePageListValidator.Builder() + .containsExactly(createdDocuments.stream() + .sorted((e1, e2) -> Integer.compare(e1.getInt("prop"), e2.getInt("prop"))) + .map(d -> d.getResourceId()).collect(Collectors.toList())) + .numberOfPages((createdDocuments.size() + 1) / 3) + .allPagesSatisfy(new FeedResponsePageValidator.Builder() + .requestChargeGreaterThanOrEqualTo(1.0).build()) + .build(); + validateQuerySuccess(queryObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT, enabled = false) + public void invalidQuerySytax() throws Exception { + + // NOTE: this test passes on Linux but not on Windows in the presence of dlls + + // ServiceJNIWrapper in DocumentClient throws IllegalArgumentException instead of DocumentClientException + // after the behavior is fixed enable this test + String query = "I am an invalid query"; + FeedOptions options = new FeedOptions(); + options.setEnableCrossPartitionQuery(true); + Observable> queryObservable = client + .queryDocuments(getCollectionLink(), query, options); + + FailureValidator validator = new FailureValidator.Builder() + .instanceOf(DocumentClientException.class) + .statusCode(400) + .notNullActivityId() + .build(); + validateQueryFailure(queryObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void crossPartitionQueryNotEnabled() throws Exception { + + String query = "SELECT * from root"; + FeedOptions options = new FeedOptions(); + Observable> queryObservable = client + .queryDocuments(getCollectionLink(), query, options); + + FailureValidator validator = new FailureValidator.Builder() + .instanceOf(DocumentClientException.class) + .statusCode(400) + .build(); + validateQueryFailure(queryObservable, validator); + } + + public static void createDocument(AsyncDocumentClient client, int cnt) throws DocumentClientException { + Document docDefinition = getDocumentDefinition(cnt); + + Document createdDocument = client + .createDocument(getCollectionLink(), docDefinition, null, false).toBlocking().single().getResource(); + createdDocuments.add(createdDocument); + } + + @BeforeSuite(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public static void beforeSuite() throws Exception { + houseKeepingClient = createRxWrapperDocumentClient().build(); + Database d = new Database(); + d.setId(DATABASE_ID); + createdDatabase = safeCreateDatabase(houseKeepingClient, d); + createdCollection = safeCreateCollection(houseKeepingClient, createdDatabase.getSelfLink(), getCollectionDefinition()); + for(int i = 0; i < 5; i++) { + createDocument(houseKeepingClient, i); + } + } + + @AfterSuite(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public static void afterSuite() { + deleteDatabase(houseKeepingClient, createdDatabase.getId()); + houseKeepingClient.close(); + } + + @BeforeSuite + public static void createDocuments() throws Exception { + + } + + @BeforeClass(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeClass() throws Exception { + // set up the client + client = clientBuilder.build(); + + } + + private static Document getDocumentDefinition(int cnt) { + String uuid = UUID.randomUUID().toString(); + Document doc = new Document(String.format("{ " + + "\"id\": \"%s\", " + + "\"prop\" : %d, " + + "\"mypk\": \"%s\", " + + "\"sgmts\": [[6519456, 1471916863], [2498434, 1455671440]]" + + "}" + , uuid, cnt, uuid)); + return doc; + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/EventLoopSizeTest.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/EventLoopSizeTest.java new file mode 100644 index 000000000000..47b82eddc368 --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/EventLoopSizeTest.java @@ -0,0 +1,134 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import java.util.UUID; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import com.microsoft.azure.documentdb.ConnectionMode; +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; +import com.microsoft.azure.documentdb.rx.ResourceResponseValidator; +import com.microsoft.azure.documentdb.rx.TestConfigurations; +import com.microsoft.azure.documentdb.rx.TestSuiteBase; +import com.microsoft.azure.documentdb.rx.Utils; + +import rx.Observable; + + +public class EventLoopSizeTest extends TestSuiteBase { + private final static String DATABASE_ID = getDatabaseId(EventLoopSizeTest.class); + + private AsyncDocumentClient client; + private Database database; + private DocumentCollection collection; + + @Test(groups = { "simple" }, timeOut = TIMEOUT, expectedExceptions = { IllegalArgumentException.class }) + public void invalidBuilder() throws Exception { + + ConnectionPolicy cp = new ConnectionPolicy(); + cp.setConnectionMode(ConnectionMode.DirectHttps); + new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(ConnectionPolicy.GetDefault()) + .withConsistencyLevel(ConsistencyLevel.Session) + .withWorkers(2, 1) + .withConnectionPolicy(cp).build(); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createDocument() throws Exception { + + AsyncDocumentClient newClient = new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(ConnectionPolicy.GetDefault()) + .withConsistencyLevel(ConsistencyLevel.Session) + .withWorkers(2, 1) + .build(); + + try { + Document docDefinition = getDocumentDefinition(); + + Observable> createObservable = newClient + .createDocument(collection.getSelfLink(), docDefinition, null, false); + + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(docDefinition.getId()) + .build(); + + validateSuccess(createObservable, validator); + } finally { + newClient.close(); + } + } + + @BeforeClass(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeClass() { + // set up the client + client = new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(ConnectionPolicy.GetDefault()) + .withConsistencyLevel(ConsistencyLevel.Session) + .build(); + + Database databaseDefinition = new Database(); + databaseDefinition.setId(DATABASE_ID); + + try { + client.deleteDatabase(Utils.getDatabaseLink(databaseDefinition, true), null).toBlocking().single(); + } catch (Exception e) { + // ignore failure if it doesn't exist + } + + database = client.createDatabase(databaseDefinition, null).toBlocking().single().getResource(); + collection = client.createCollection(database.getSelfLink(), getCollectionDefinition(), null).toBlocking().single().getResource(); + } + + private Document getDocumentDefinition() { + String uuid = UUID.randomUUID().toString(); + Document doc = new Document(String.format("{ " + + "\"id\": \"%s\", " + + "\"mypk\": \"%s\", " + + "\"sgmts\": [[6519456, 1471916863], [2498434, 1455671440]]" + + "}" + , uuid, uuid)); + return doc; + } + + @AfterClass(groups = { "simple" }, timeOut = SHUTDOWN_TIMEOUT) + public void afterClass() { + client.deleteDatabase(database.getSelfLink(), null).toBlocking().single(); + client.close(); + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/FailureValidator.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/FailureValidator.java new file mode 100644 index 000000000000..04be35c61eed --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/FailureValidator.java @@ -0,0 +1,166 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.Error; + +public interface FailureValidator { + + void validate(Throwable t); + + class Builder { + private List validators = new ArrayList<>(); + + public FailureValidator build() { + return new FailureValidator() { + @Override + public void validate(Throwable t) { + for (FailureValidator validator : validators) { + validator.validate(t); + } + } + }; + } + + public Builder statusCode(int statusCode) { + validators.add(new FailureValidator() { + @Override + public void validate(Throwable t) { + assertThat(t).isNotNull(); + assertThat(t).isInstanceOf(DocumentClientException.class); + assertThat(((DocumentClientException) t).getStatusCode()).isEqualTo(statusCode); + } + }); + return this; + } + + public Builder errorMessageContain(int statusCode) { + validators.add(new FailureValidator() { + @Override + public void validate(Throwable t) { + assertThat(t).isNotNull(); + assertThat(t).isInstanceOf(DocumentClientException.class); + assertThat(((DocumentClientException) t).getStatusCode()).isEqualTo(statusCode); + } + }); + return this; + } + + public Builder notNullActivityId() { + validators.add(new FailureValidator() { + @Override + public void validate(Throwable t) { + assertThat(t).isNotNull(); + t.printStackTrace(); + assertThat(t).isInstanceOf(DocumentClientException.class); + assertThat(((DocumentClientException) t).getActivityId()).isNotNull(); + } + }); + return this; + } + + public Builder error(Error error) { + validators.add(new FailureValidator() { + @Override + public void validate(Throwable t) { + assertThat(t).isNotNull(); + t.printStackTrace(); + assertThat(t).isInstanceOf(DocumentClientException.class); + assertThat(((DocumentClientException) t).getError().toJson()).isEqualTo(error.toJson()); + } + }); + return this; + } + + public Builder subStatusCode(int substatusCode) { + validators.add(new FailureValidator() { + @Override + public void validate(Throwable t) { + assertThat(t).isNotNull(); + assertThat(t).isInstanceOf(DocumentClientException.class); + assertThat(((DocumentClientException) t).getSubStatusCode()).isEqualTo(substatusCode); + } + }); + return this; + } + + public Builder instanceOf(Class cls) { + validators.add(new FailureValidator() { + @Override + public void validate(Throwable t) { + assertThat(t).isNotNull(); + assertThat(t).isInstanceOf(cls); + } + }); + return this; + } + + public Builder resourceNotFound() { + + validators.add(new FailureValidator() { + @Override + public void validate(Throwable t) { + assertThat(t).isNotNull(); + assertThat(t).isInstanceOf(DocumentClientException.class); + DocumentClientException ex = (DocumentClientException) t; + assertThat(ex.getStatusCode()).isEqualTo(404); + + } + }); + return this; + } + + public Builder resourceAlreadyExists() { + + validators.add(new FailureValidator() { + @Override + public void validate(Throwable t) { + assertThat(t).isNotNull(); + assertThat(t).isInstanceOf(DocumentClientException.class); + DocumentClientException ex = (DocumentClientException) t; + assertThat(ex.getStatusCode()).isEqualTo(409); + + } + }); + return this; + } + + public Builder causeInstanceOf(Class cls) { + validators.add(new FailureValidator() { + @Override + public void validate(Throwable t) { + assertThat(t).isNotNull(); + assertThat(t.getCause()).isNotNull(); + assertThat(t.getCause()).isInstanceOf(cls); + } + }); + return this; + } + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/FeedResponsePageListValidator.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/FeedResponsePageListValidator.java new file mode 100644 index 000000000000..0b3b6d9085cf --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/FeedResponsePageListValidator.java @@ -0,0 +1,115 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.Resource; + +public interface FeedResponsePageListValidator { + + void validate(List> feedList); + + class Builder { + private List> validators = new ArrayList<>(); + + public FeedResponsePageListValidator build() { + return new FeedResponsePageListValidator() { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void validate(List> feedList) { + for (FeedResponsePageListValidator validator : validators) { + validator.validate(feedList); + } + } + }; + } + + public Builder totalSize(final int expectedCount) { + validators.add(new FeedResponsePageListValidator() { + @Override + public void validate(List> feedList) { + int resultCount = feedList.stream().mapToInt(f -> f.getResults().size()).sum(); + assertThat(resultCount).isEqualTo(expectedCount); + } + }); + return this; + } + + public Builder containsExactly(List expectedIds) { + validators.add(new FeedResponsePageListValidator() { + @Override + public void validate(List> feedList) { + List actualIds = feedList + .stream() + .flatMap(f -> f.getResults().stream()) + .map(r -> r.getResourceId()) + .collect(Collectors.toList()); + assertThat(actualIds).containsExactlyElementsOf(expectedIds); + } + }); + return this; + } + + public Builder numberOfPages(int expectedNumberOfPages) { + validators.add(new FeedResponsePageListValidator() { + @Override + public void validate(List> feedList) { + assertThat(feedList).hasSize(expectedNumberOfPages); + } + }); + return this; + } + + public Builder pageSatisfy(int pageNumber, FeedResponsePageValidator pageValidator) { + validators.add(new FeedResponsePageListValidator() { + @Override + public void validate(List> feedList) { + assertThat(feedList.size()).isGreaterThan(pageNumber); + pageValidator.validate(feedList.get(pageNumber)); + } + }); + return this; + } + + public Builder allPagesSatisfy(FeedResponsePageValidator pageValidator) { + validators.add(new FeedResponsePageListValidator() { + @Override + public void validate(List> feedList) { + + for(FeedResponsePage fp: feedList) { + pageValidator.validate(fp); + } + + } + }); + return this; + } + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/FeedResponsePageValidator.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/FeedResponsePageValidator.java new file mode 100644 index 000000000000..70aaa792cd65 --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/FeedResponsePageValidator.java @@ -0,0 +1,112 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2017 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.Resource; + +interface FeedResponsePageValidator { + + void validate(FeedResponsePage feedList); + + class Builder { + private List> validators = new ArrayList<>(); + + public FeedResponsePageValidator build() { + return new FeedResponsePageValidator() { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void validate(FeedResponsePage feedPage) { + for (FeedResponsePageValidator validator : validators) { + validator.validate(feedPage); + } + } + }; + } + + public Builder pageSizeOf(final int expectedCount) { + + validators.add(new FeedResponsePageValidator() { + @Override + public void validate(FeedResponsePage feedPage) { + assertThat(feedPage.getResults()).hasSize(expectedCount); + } + }); + return this; + } + + public Builder positiveRequestCharge() { + + validators.add(new FeedResponsePageValidator() { + @Override + public void validate(FeedResponsePage feedPage) { + assertThat(feedPage.getRequestCharge()).isPositive(); + } + }); + return this; + } + + public Builder requestChargeGreaterThanOrEqualTo(double minRequestCharge) { + + validators.add(new FeedResponsePageValidator() { + @Override + public void validate(FeedResponsePage feedPage) { + assertThat(feedPage.getRequestCharge()).isGreaterThanOrEqualTo(minRequestCharge); + } + }); + return this; + } + + public Builder requestChargeLessThanOrEqualTo(double maxRequestCharge) { + + validators.add(new FeedResponsePageValidator() { + @Override + public void validate(FeedResponsePage feedPage) { + assertThat(feedPage.getRequestCharge()).isLessThanOrEqualTo(maxRequestCharge); + } + }); + return this; + } + + public Builder idsExactlyAre(final List expectedIds) { + validators.add(new FeedResponsePageValidator() { + @Override + public void validate(FeedResponsePage feedPage) { + assertThat(feedPage + .getResults().stream() + .map(r -> r.getResourceId()) + .collect(Collectors.toList())) + .containsExactlyElementsOf(expectedIds); + } + }); + return this; + } + } +} \ No newline at end of file diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/ResourceResponseValidator.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/ResourceResponseValidator.java new file mode 100644 index 000000000000..88f28b85804b --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/ResourceResponseValidator.java @@ -0,0 +1,157 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.assertj.core.api.Condition; + +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.IndexingMode; +import com.microsoft.azure.documentdb.Resource; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.StoredProcedure; + +public interface ResourceResponseValidator { + + void validate(ResourceResponse resourceResponse); + + class Builder { + private List> validators = new ArrayList<>(); + + public ResourceResponseValidator build() { + return new ResourceResponseValidator() { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void validate(ResourceResponse resourceResponse) { + for (ResourceResponseValidator validator : validators) { + validator.validate(resourceResponse); + } + } + }; + } + + public Builder withId(final String resourceId) { + validators.add(new ResourceResponseValidator() { + + @Override + public void validate(ResourceResponse resourceResponse) { + assertThat(resourceResponse.getResource()).isNotNull(); + assertThat(resourceResponse.getResource().getId()).as("check Resource Id").isEqualTo(resourceId); + } + }); + return this; + } + + public Builder nullResource() { + validators.add(new ResourceResponseValidator() { + + @Override + public void validate(ResourceResponse resourceResponse) { + assertThat(resourceResponse.getResource()).isNull(); + } + }); + return this; + } + + public Builder withProperty(String propertyName, Condition validatingCondition) { + validators.add(new ResourceResponseValidator() { + + @Override + public void validate(ResourceResponse resourceResponse) { + assertThat(resourceResponse.getResource()).isNotNull(); + assertThat(resourceResponse.getResource().get(propertyName)).is(validatingCondition); + + } + }); + return this; + } + + public Builder withProperty(String propertyName, Object value) { + validators.add(new ResourceResponseValidator() { + + @Override + public void validate(ResourceResponse resourceResponse) { + assertThat(resourceResponse.getResource()).isNotNull(); + assertThat(resourceResponse.getResource().get(propertyName)).isEqualTo(value); + + } + }); + return this; + } + + + public Builder indexingMode(IndexingMode mode) { + validators.add(new ResourceResponseValidator() { + + @Override + public void validate(ResourceResponse resourceResponse) { + assertThat(resourceResponse.getResource()).isNotNull(); + assertThat(resourceResponse.getResource().getIndexingPolicy()).isNotNull(); + assertThat(resourceResponse.getResource().getIndexingPolicy().getIndexingMode()).isEqualTo(mode); + } + }); + return this; + } + + public Builder withBody(String storedProcedureFunction) { + validators.add(new ResourceResponseValidator() { + + @Override + public void validate(ResourceResponse resourceResponse) { + assertThat(resourceResponse.getResource().getBody()).isEqualTo(storedProcedureFunction); + } + }); + return this; + } + + public Builder notNullEtag() { + validators.add(new ResourceResponseValidator() { + + @Override + public void validate(ResourceResponse resourceResponse) { + assertThat(resourceResponse.getResource()).isNotNull(); + assertThat(resourceResponse.getResource().getETag()).isNotNull(); + } + }); + return this; + } + + public Builder validatePropertyCondition(String key, Condition condition) { + validators.add(new ResourceResponseValidator() { + + @Override + public void validate(ResourceResponse resourceResponse) { + assertThat(resourceResponse.getResource()).isNotNull(); + assertThat(resourceResponse.getResource().get(key)).is(condition); + + } + }); + return this; + } + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/StoredProcedureCrudTest.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/StoredProcedureCrudTest.java new file mode 100644 index 000000000000..ce30b43bf654 --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/StoredProcedureCrudTest.java @@ -0,0 +1,145 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import java.util.UUID; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterSuite; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; + +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.StoredProcedure; + +import rx.Observable; + +public class StoredProcedureCrudTest extends TestSuiteBase { + + public final static String DATABASE_ID = getDatabaseId(StoredProcedureCrudTest.class); + + private static AsyncDocumentClient houseKeepingClient; + private static Database createdDatabase; + private static DocumentCollection createdCollection; + + private AsyncDocumentClient.Builder clientBuilder; + private AsyncDocumentClient client; + + @Factory(dataProvider = "clientBuilders") + public StoredProcedureCrudTest(AsyncDocumentClient.Builder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createStoredProcedure() throws Exception { + + // create a stored procedure + StoredProcedure storedProcedureDef = new StoredProcedure(); + storedProcedureDef.setId(UUID.randomUUID().toString()); + storedProcedureDef.setBody("function() {var x = 10;}"); + + Observable> createObservable = client.createStoredProcedure(getCollectionLink(), storedProcedureDef, null); + + // validate stored procedure creation + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(storedProcedureDef.getId()) + .withBody("function() {var x = 10;}") + .notNullEtag() + .build(); + validateSuccess(createObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void readStoredProcedure() throws Exception { + // create a stored procedure + StoredProcedure storedProcedureDef = new StoredProcedure(); + storedProcedureDef.setId(UUID.randomUUID().toString()); + storedProcedureDef.setBody("function() {var x = 10;}"); + StoredProcedure storedProcedure = client.createStoredProcedure(getCollectionLink(), storedProcedureDef, null).toBlocking().single().getResource(); + + // read stored procedure + Observable> readObservable = client.readStoredProcedure(storedProcedure.getSelfLink(), null); + + + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(storedProcedureDef.getId()) + .withBody("function() {var x = 10;}") + .notNullEtag() + .build(); + validateSuccess(readObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void deleteStoredProcedure() throws Exception { + // create a stored procedure + StoredProcedure storedProcedureDef = new StoredProcedure(); + storedProcedureDef.setId(UUID.randomUUID().toString()); + storedProcedureDef.setBody("function() {var x = 10;}"); + StoredProcedure storedProcedure = client.createStoredProcedure(getCollectionLink(), storedProcedureDef, null).toBlocking().single().getResource(); + + // delete + Observable> deleteObservable = client.deleteStoredProcedure(storedProcedure.getSelfLink(), null); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .nullResource() + .build(); + validateSuccess(deleteObservable, validator); + + //TODO validate after deletion the resource is actually deleted (not found) + } + + @BeforeClass(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeClass() { + this.client = this.clientBuilder.build(); + } + + @AfterClass(groups = { "simple" }, timeOut = SHUTDOWN_TIMEOUT) + public void afterClass() { + this.client.close(); + } + + @BeforeSuite(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public static void beforeSuite() { + houseKeepingClient = createGatewayRxDocumentClient().build(); + Database d = new Database(); + d.setId(DATABASE_ID); + createdDatabase = safeCreateDatabase(houseKeepingClient, d); + createdCollection = safeCreateCollection(houseKeepingClient, createdDatabase.getSelfLink(), getCollectionDefinition()); + } + + @AfterSuite(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public static void afterSuite() { + + deleteDatabase(houseKeepingClient, createdDatabase.getId()); + houseKeepingClient.close(); + } + + private String getCollectionLink() { + return createdCollection.getSelfLink(); + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/TestConfigurations.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/TestConfigurations.java new file mode 100644 index 000000000000..89b4ebcd3690 --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/TestConfigurations.java @@ -0,0 +1,35 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +/** + * Contains the configurations for test file + */ +public final class TestConfigurations { + // Replace MASTER_KEY and HOST with values from your DocumentDB account. + // The default values are credentials of the local emulator, which are not used in any production environment. + // + public static final String MASTER_KEY = + "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + public static final String HOST = "https://localhost:443/"; +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/TestSuiteBase.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/TestSuiteBase.java new file mode 100644 index 000000000000..3c5c2d70f6bb --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/TestSuiteBase.java @@ -0,0 +1,250 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2017 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.testng.annotations.DataProvider; + +import com.microsoft.azure.documentdb.ConnectionMode; +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.DocumentClient; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.FeedResponsePage; +import com.microsoft.azure.documentdb.PartitionKeyDefinition; +import com.microsoft.azure.documentdb.RequestOptions; +import com.microsoft.azure.documentdb.Resource; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.internal.directconnectivity.HttpClientFactory; +import com.microsoft.azure.documentdb.rx.internal.RxWrapperDocumentClientImpl; + +import rx.Observable; +import rx.observers.TestSubscriber; + +public class TestSuiteBase { + + protected static final int TIMEOUT = 4000; + protected static final int SETUP_TIMEOUT = 6000; + protected static final int SHUTDOWN_TIMEOUT = 6000; + + protected int subscriberValidationTimeout = TIMEOUT; + + static { + HttpClientFactory.DISABLE_HOST_NAME_VERIFICATION = true; + } + + public static String getDatabaseId(Class klass) { + return String.format("java.rx.%s", klass.getName()); + } + + public static DocumentCollection createCollection(AsyncDocumentClient client, String databaseLink, DocumentCollection collection) { + return client.createCollection( + databaseLink, collection, null) + .toBlocking().single().getResource(); + } + + public static DocumentCollection safeCreateCollection(AsyncDocumentClient client, String databaseLink, DocumentCollection collection) { + deleteCollectionIfExists(client, databaseLink, collection.getId()); + return createCollection(client, databaseLink, collection); + } + + public static String getCollectionLink(DocumentCollection collection) { + return collection.getSelfLink(); + } + + static protected DocumentCollection getCollectionDefinition() { + PartitionKeyDefinition partitionKeyDef = new PartitionKeyDefinition(); + ArrayList paths = new ArrayList(); + paths.add("/mypk"); + partitionKeyDef.setPaths(paths); + + RequestOptions options = new RequestOptions(); + options.setOfferThroughput(10100); + DocumentCollection collectionDefinition = new DocumentCollection(); + collectionDefinition.setId(UUID.randomUUID().toString()); + collectionDefinition.setPartitionKey(partitionKeyDef); + + return collectionDefinition; + } + + public static void deleteCollectionIfExists(AsyncDocumentClient client, String databaseLink, String collectionId) { + List res = client.queryCollections(databaseLink, + String.format("SELECT * FROM root r where r.id = '%s'", collectionId), null).toBlocking().single().getResults(); + if (!res.isEmpty()) { + deleteCollection(client, Utils.getCollectionNameLink(databaseLink, collectionId)); + } + } + + public static void deleteCollection(AsyncDocumentClient client, String collectionLink) { + client.deleteCollection(collectionLink, null).toBlocking().single(); + } + + public static String getDatabaseLink(Database database) { + return database.getSelfLink(); + } + + static protected Database safeCreateDatabase(AsyncDocumentClient client, Database database) { + try { + deleteDatabase(client, database.getId()); + } catch (Exception e) { + } + return createDatabase(client, database); + } + + static private Database createDatabase(AsyncDocumentClient client, Database database) { + Observable> databaseObservable = client.createDatabase(database, + null); + return databaseObservable.toBlocking().single().getResource(); + } + + static protected Database createDatabase(AsyncDocumentClient client, String databaseId) { + Database databaseDefinition = new Database(); + databaseDefinition.setId(databaseId); + return createDatabase(client, databaseDefinition); + } + + static protected void deleteDatabase(AsyncDocumentClient client, String databaseId) { + client.deleteDatabase(Utils.getDatabaseNameLink(databaseId), null).toBlocking().single(); + } + + public void validateSuccess(Observable> observable, + ResourceResponseValidator validator) throws InterruptedException { + validateSuccess(observable, validator, subscriberValidationTimeout); + } + + public static void validateSuccess(Observable> observable, + ResourceResponseValidator validator, long timeout) throws InterruptedException { + + TestSubscriber> testSubscriber = new TestSubscriber>(); + + observable.subscribe(testSubscriber); + testSubscriber.awaitTerminalEvent(timeout, TimeUnit.MILLISECONDS); + testSubscriber.assertNoErrors(); + testSubscriber.assertCompleted(); + testSubscriber.assertValueCount(1); + validator.validate(testSubscriber.getOnNextEvents().get(0)); + } + + public void validateFailure(Observable> observable, + FailureValidator validator) + throws InterruptedException { + validateFailure(observable, validator, subscriberValidationTimeout); + } + + public static void validateFailure(Observable> observable, + FailureValidator validator, long timeout) + throws InterruptedException { + + TestSubscriber> testSubscriber = new TestSubscriber<>(); + + observable.subscribe(testSubscriber); + testSubscriber.awaitTerminalEvent(timeout, TimeUnit.MILLISECONDS); + testSubscriber.assertNotCompleted(); + testSubscriber.assertTerminalEvent(); + assertThat(testSubscriber.getOnErrorEvents()).hasSize(1); + validator.validate(testSubscriber.getOnErrorEvents().get(0)); + } + + public void validateQuerySuccess(Observable> observable, + FeedResponsePageListValidator validator) throws InterruptedException { + validateQuerySuccess(observable, validator, subscriberValidationTimeout); + } + + public static void validateQuerySuccess(Observable> observable, + FeedResponsePageListValidator validator, long timeout) throws InterruptedException { + + TestSubscriber> testSubscriber = new TestSubscriber<>(); + + observable.subscribe(testSubscriber); + testSubscriber.awaitTerminalEvent(timeout, TimeUnit.MILLISECONDS); + testSubscriber.assertNoErrors(); + testSubscriber.assertCompleted(); + validator.validate(testSubscriber.getOnNextEvents()); + } + + public void validateQueryFailure(Observable> observable, + FailureValidator validator) + throws InterruptedException { + validateQueryFailure(observable, validator, subscriberValidationTimeout); + } + + public static void validateQueryFailure(Observable> observable, + FailureValidator validator, long timeout) + throws InterruptedException { + + TestSubscriber> testSubscriber = new TestSubscriber<>(); + + observable.subscribe(testSubscriber); + testSubscriber.awaitTerminalEvent(timeout, TimeUnit.MILLISECONDS); + testSubscriber.assertNotCompleted(); + testSubscriber.assertTerminalEvent(); + assertThat(testSubscriber.getOnErrorEvents()).hasSize(1); + validator.validate(testSubscriber.getOnErrorEvents().get(0)); + } + + @DataProvider + public static Object[][] clientBuilders() { + return new Object[][] { { createGatewayRxDocumentClient() }, { createDirectHttpsRxDocumentClient() } }; + } + + static protected AsyncDocumentClient.Builder createGatewayRxDocumentClient() { + ConnectionPolicy connectionPolicy = new ConnectionPolicy(); + connectionPolicy.setConnectionMode(ConnectionMode.Gateway); + return new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(connectionPolicy) + .withConsistencyLevel(ConsistencyLevel.Session); + } + + static protected AsyncDocumentClient.Builder createDirectHttpsRxDocumentClient() { + ConnectionPolicy connectionPolicy = new ConnectionPolicy(); + connectionPolicy.setConnectionMode(ConnectionMode.DirectHttps); + return new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(connectionPolicy) + .withConsistencyLevel(ConsistencyLevel.Session); + } + + static protected AsyncDocumentClient.Builder createRxWrapperDocumentClient() { + + return new AsyncDocumentClient.Builder() { + /* (non-Javadoc) + * @see com.microsoft.azure.documentdb.rx.AsyncDocumentClient.Builder#build() + */ + @Override + public AsyncDocumentClient build() { + return new RxWrapperDocumentClientImpl(new DocumentClient(TestConfigurations.HOST, + TestConfigurations.MASTER_KEY, ConnectionPolicy.GetDefault(), ConsistencyLevel.Session)); + } + }; + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/Utils.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/Utils.java new file mode 100644 index 000000000000..35f0d16a7f07 --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/Utils.java @@ -0,0 +1,55 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx; + +import com.microsoft.azure.documentdb.Database; + +public class Utils { + private static final String DATABASES_PATH_SEGMENT = "dbs"; + private static final String COLLECTIONS_PATH_SEGMENT = "colls"; + + + public static String getDatabaseLink(Database database, boolean isNameBased) { + if (isNameBased) { + return getDatabaseNameLink(database.getId()); + } else { + return database.getSelfLink(); + } + } + + public static String getDatabaseNameLink(String databaseId) { + return DATABASES_PATH_SEGMENT + "/" + databaseId; + } + + public static String getCollectionNameLink(String databaseId, String collectionId) { + + if (collectionId.equals("/")) { + return databaseId + "/" + COLLECTIONS_PATH_SEGMENT + "/" + collectionId; + + } else { + return DATABASES_PATH_SEGMENT + "/" + databaseId + "/" + COLLECTIONS_PATH_SEGMENT + "/" + collectionId; + } + } + + private Utils() {} +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/internal/RetryCreateDocumentTest.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/internal/RetryCreateDocumentTest.java new file mode 100644 index 000000000000..cdacafd92713 --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/internal/RetryCreateDocumentTest.java @@ -0,0 +1,217 @@ +package com.microsoft.azure.documentdb.rx.internal; + +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Mockito.doAnswer; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.Error; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.internal.DocumentServiceResponse; +import com.microsoft.azure.documentdb.internal.HttpConstants; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; +import com.microsoft.azure.documentdb.rx.FailureValidator; +import com.microsoft.azure.documentdb.rx.ResourceResponseValidator; +import com.microsoft.azure.documentdb.rx.TestConfigurations; +import com.microsoft.azure.documentdb.rx.TestSuiteBase; +import com.microsoft.azure.documentdb.rx.Utils; + +import rx.Observable; + +public class RetryCreateDocumentTest extends TestSuiteBase { + private final static String DATABASE_ID = getDatabaseId(RetryCreateDocumentTest.class); + + private final static int TIMEOUT = 7000; + { + subscriberValidationTimeout = TIMEOUT; + } + + private AsyncDocumentClient client; + + private Database database; + private DocumentCollection collection; + private RxGatewayStoreModel gateway; + private RxGatewayStoreModel spyGateway; + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void retryDocumentCreate() throws Exception { + // create a document to ensure collection is cached + client.createDocument(collection.getSelfLink(), getDocumentDefinition(), null, false).toBlocking().single(); + + Document docDefinition = getDocumentDefinition(); + + Observable> createObservable = client + .createDocument(collection.getSelfLink(), docDefinition, null, false); + AtomicInteger count = new AtomicInteger(); + + doAnswer(new Answer< Observable>() { + @Override + public Observable answer(InvocationOnMock invocation) throws Throwable { + RxDocumentServiceRequest req = (RxDocumentServiceRequest) invocation.getArguments()[0]; + int currentAttempt = count.getAndIncrement(); + if (currentAttempt == 0) { + Map header = ImmutableMap.of( + HttpConstants.HttpHeaders.SUB_STATUS, + Integer.toString(HttpConstants.SubStatusCodes.PARTITION_KEY_MISMATCH)); + + return Observable.error(new DocumentClientException(HttpConstants.StatusCodes.BADREQUEST, new Error() , header)); + } else { + return gateway.doCreate(req); + } + } + }).when(this.spyGateway).doCreate(anyObject()); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(docDefinition.getId()).build(); + validateSuccess(createObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createDocument_noRetryOnNonRetriableFailure() throws Exception { + // create a document to ensure collection is cached + client.createDocument(collection.getSelfLink(), getDocumentDefinition(), null, false).toBlocking().single(); + + Document docDefinition = getDocumentDefinition(); + + Observable> createObservable = client + .createDocument(collection.getSelfLink(), docDefinition, null, false); + AtomicInteger count = new AtomicInteger(); + + doAnswer(new Answer< Observable>() { + @Override + public Observable answer(InvocationOnMock invocation) throws Throwable { + RxDocumentServiceRequest req = (RxDocumentServiceRequest) invocation.getArguments()[0]; + int currentAttempt = count.getAndIncrement(); + if (currentAttempt == 0) { + Map header = ImmutableMap.of( + HttpConstants.HttpHeaders.SUB_STATUS, + Integer.toString(2)); + + return Observable.error(new DocumentClientException(1, new Error() , header)); + } else { + return gateway.doCreate(req); + } + } + }).when(this.spyGateway).doCreate(anyObject()); + + // validate + + FailureValidator validator = new FailureValidator.Builder().statusCode(1).subStatusCode(2).build(); + validateFailure(createObservable, validator); + } + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void createDocument_failImmediatelyOnNonRetriable() throws Exception { + // create a document to ensure collection is cached + client.createDocument(collection.getSelfLink(), getDocumentDefinition(), null, false).toBlocking().single(); + + Document docDefinition = getDocumentDefinition(); + + Observable> createObservable = client + .createDocument(collection.getSelfLink(), docDefinition, null, false); + AtomicInteger count = new AtomicInteger(); + + doAnswer(new Answer< Observable>() { + @Override + public Observable answer(InvocationOnMock invocation) throws Throwable { + RxDocumentServiceRequest req = (RxDocumentServiceRequest) invocation.getArguments()[0]; + int currentAttempt = count.getAndIncrement(); + if (currentAttempt == 0) { + Map header = ImmutableMap.of( + HttpConstants.HttpHeaders.SUB_STATUS, + Integer.toString(2)); + + return Observable.error(new DocumentClientException(1, new Error() , header)); + } else { + return gateway.doCreate(req); + } + } + }).when(this.spyGateway).doCreate(anyObject()); + + // validate + + FailureValidator validator = new FailureValidator.Builder().statusCode(1).subStatusCode(2).build(); + validateFailure(createObservable.timeout(100, TimeUnit.MILLISECONDS), validator); + } + + private void registerSpyProxy() { + + RxDocumentClientImpl clientImpl = (RxDocumentClientImpl) client; + try { + Field f = RxDocumentClientImpl.class.getDeclaredField("gatewayProxy"); + f.setAccessible(true); + this.gateway = (RxGatewayStoreModel) f.get(clientImpl); + this.spyGateway = Mockito.spy(gateway); + f.set(clientImpl, this.spyGateway); + } catch (Exception e) { + fail("failed to register spy proxy due to " + e.getMessage()); + } + } + + @BeforeMethod + public void beforeMethod() { + Mockito.reset(this.spyGateway); + } + + @BeforeClass(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeClass() { + // set up the client + client = new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(ConnectionPolicy.GetDefault()) + .withConsistencyLevel(ConsistencyLevel.Session) + .build(); + registerSpyProxy(); + + Database databaseDefinition = new Database(); + databaseDefinition.setId(DATABASE_ID); + + try { + client.deleteDatabase(Utils.getDatabaseLink(databaseDefinition, true), null).toBlocking().single(); + } catch (Exception e) { + // ignore failure if it doesn't exist + } + + database = client.createDatabase(databaseDefinition, null).toBlocking().single().getResource(); + collection = client.createCollection(database.getSelfLink(), getCollectionDefinition(), null).toBlocking().single().getResource(); + } + + private Document getDocumentDefinition() { + String uuid = UUID.randomUUID().toString(); + Document doc = new Document(String.format("{ " + + "\"id\": \"%s\", " + + "\"mypk\": \"%s\", " + + "\"sgmts\": [[6519456, 1471916863], [2498434, 1455671440]]" + + "}" + , uuid, uuid)); + return doc; + } + + @AfterClass(groups = { "simple" }, timeOut = SHUTDOWN_TIMEOUT) + public void afterClass() { + client.deleteDatabase(database.getSelfLink(), null).toBlocking().single(); + client.close(); + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/internal/RetryThrottleTest.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/internal/RetryThrottleTest.java new file mode 100644 index 000000000000..df3e484561fb --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/internal/RetryThrottleTest.java @@ -0,0 +1,139 @@ +package com.microsoft.azure.documentdb.rx.internal; + +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Mockito.doAnswer; + +import java.lang.reflect.Field; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.microsoft.azure.documentdb.ConnectionPolicy; +import com.microsoft.azure.documentdb.ConsistencyLevel; +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.ResourceResponse; +import com.microsoft.azure.documentdb.internal.DocumentServiceResponse; +import com.microsoft.azure.documentdb.internal.HttpConstants; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; +import com.microsoft.azure.documentdb.rx.ResourceResponseValidator; +import com.microsoft.azure.documentdb.rx.TestConfigurations; +import com.microsoft.azure.documentdb.rx.TestSuiteBase; +import com.microsoft.azure.documentdb.rx.Utils; + +import rx.Observable; + +public class RetryThrottleTest extends TestSuiteBase { + private final static String DATABASE_ID = getDatabaseId(RetryThrottleTest.class); + + private final static int TIMEOUT = 7000; + { + subscriberValidationTimeout = TIMEOUT; + } + + private AsyncDocumentClient client; + private Database database; + private DocumentCollection collection; + private RxGatewayStoreModel gateway; + private RxGatewayStoreModel spyGateway; + + @Test(groups = { "simple" }, timeOut = TIMEOUT) + public void retryDocumentCreate() throws Exception { + // create a document to ensure collection is cached + client.createDocument(collection.getSelfLink(), getDocumentDefinition(), null, false).toBlocking().single(); + + Document docDefinition = getDocumentDefinition(); + + Observable> createObservable = client + .createDocument(collection.getSelfLink(), docDefinition, null, false); + AtomicInteger count = new AtomicInteger(); + + doAnswer(new Answer< Observable>() { + @Override + public Observable answer(InvocationOnMock invocation) throws Throwable { + RxDocumentServiceRequest req = (RxDocumentServiceRequest) invocation.getArguments()[0]; + int currentAttempt = count.getAndIncrement(); + if (currentAttempt == 0) { + return Observable.error(new DocumentClientException(HttpConstants.StatusCodes.TOO_MANY_REQUESTS)); + } else { + return gateway.doCreate(req); + } + } + }).when(this.spyGateway).doCreate(anyObject()); + + // validate + ResourceResponseValidator validator = new ResourceResponseValidator.Builder() + .withId(docDefinition.getId()).build(); + validateSuccess(createObservable, validator); + } + + private void registerSpyProxy() { + + RxDocumentClientImpl clientImpl = (RxDocumentClientImpl) client; + try { + Field f = RxDocumentClientImpl.class.getDeclaredField("gatewayProxy"); + f.setAccessible(true); + this.gateway = (RxGatewayStoreModel) f.get(clientImpl); + this.spyGateway = Mockito.spy(gateway); + f.set(clientImpl, this.spyGateway); + } catch (Exception e) { + fail("failed to register spy proxy due to " + e.getMessage()); + } + } + + @BeforeMethod + public void beforeMethod() { + Mockito.reset(this.spyGateway); + } + + @BeforeClass(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeClass() { + // set up the client + client = new AsyncDocumentClient.Builder() + .withServiceEndpoint(TestConfigurations.HOST) + .withMasterKey(TestConfigurations.MASTER_KEY) + .withConnectionPolicy(ConnectionPolicy.GetDefault()) + .withConsistencyLevel(ConsistencyLevel.Session) + .build(); + registerSpyProxy(); + + Database databaseDefinition = new Database(); + databaseDefinition.setId(DATABASE_ID); + + try { + client.deleteDatabase(Utils.getDatabaseLink(databaseDefinition, true), null).toBlocking().single(); + } catch (Exception e) { + // ignore failure if it doesn't exist + } + + database = client.createDatabase(databaseDefinition, null).toBlocking().single().getResource(); + collection = client.createCollection(database.getSelfLink(), getCollectionDefinition(), null).toBlocking().single().getResource(); + } + + private Document getDocumentDefinition() { + String uuid = UUID.randomUUID().toString(); + Document doc = new Document(String.format("{ " + + "\"id\": \"%s\", " + + "\"mypk\": \"%s\", " + + "\"sgmts\": [[6519456, 1471916863], [2498434, 1455671440]]" + + "}" + , uuid, uuid)); + return doc; + } + + @AfterClass(groups = { "simple" }, timeOut = SHUTDOWN_TIMEOUT) + public void afterClass() { + client.deleteDatabase(database.getSelfLink(), null).toBlocking().single(); + client.close(); + } +} diff --git a/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/internal/SessionTest.java b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/internal/SessionTest.java new file mode 100644 index 000000000000..1b0fa2cc2b28 --- /dev/null +++ b/azure-documentdb-rx/src/test/java/com/microsoft/azure/documentdb/rx/internal/SessionTest.java @@ -0,0 +1,150 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.microsoft.azure.documentdb.rx.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doAnswer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import com.microsoft.azure.documentdb.Database; +import com.microsoft.azure.documentdb.Document; +import com.microsoft.azure.documentdb.DocumentClientException; +import com.microsoft.azure.documentdb.DocumentCollection; +import com.microsoft.azure.documentdb.internal.DocumentServiceResponse; +import com.microsoft.azure.documentdb.internal.HttpConstants; +import com.microsoft.azure.documentdb.rx.AsyncDocumentClient; +import com.microsoft.azure.documentdb.rx.DocumentCrudTest; +import com.microsoft.azure.documentdb.rx.TestSuiteBase; + +public class SessionTest extends TestSuiteBase { + public final static String DATABASE_ID = getDatabaseId(DocumentCrudTest.class); + + private AsyncDocumentClient houseKeepingClient; + private Database createdDatabase; + private DocumentCollection createdCollection; + + private RxDocumentClientImpl client1; + private RxDocumentClientImpl client2; + + private String getCollectionLink() { + return createdCollection.getSelfLink(); + } + + @BeforeClass(groups = { "simple" }, timeOut = SETUP_TIMEOUT) + public void beforeClass() { + + houseKeepingClient = createRxWrapperDocumentClient().build(); + Database d = new Database(); + d.setId(DATABASE_ID); + createdDatabase = safeCreateDatabase(client1, d); + + DocumentCollection cl = new DocumentCollection(); + cl.setId(UUID.randomUUID().toString()); + createdCollection = safeCreateCollection(houseKeepingClient, createdDatabase.getSelfLink(), cl); + } + + @AfterClass(groups = { "simple" }, timeOut = SHUTDOWN_TIMEOUT) + public void afterClass() { + deleteDatabase(houseKeepingClient, createdDatabase.getId()); + houseKeepingClient.close(); + } + + @BeforeTest + public void beforeTest() { + client1 = (RxDocumentClientImpl) createGatewayRxDocumentClient().build(); + client2 = (RxDocumentClientImpl) createGatewayRxDocumentClient().build(); + } + + @AfterTest + public void afterTest() { + client1.close(); + client2.close(); + } + + @Test + public void testSessionConsistency_ReadYourWrites() throws DocumentClientException { + RxDocumentClientImpl clientUnderTest = Mockito.spy(client1); + + List capturedRequestSessionTokenList = Collections.synchronizedList(new ArrayList()); + List capturedResponseSessionTokenList = Collections.synchronizedList(new ArrayList()); + + clientUnderTest.readCollection(getCollectionLink(), null).toBlocking().single(); + clientUnderTest.createDocument( + getCollectionLink(), new Document(), null, false).toBlocking().single(); + + setupSpySession(capturedRequestSessionTokenList, capturedResponseSessionTokenList, clientUnderTest, client1); + + for (int i = 0; i < 10; i++) { + + Document documentCreated = clientUnderTest.createDocument( + getCollectionLink(), new Document(), null, false).toBlocking().single().getResource(); + + assertThat(capturedRequestSessionTokenList).hasSize(3*i+1); + assertThat(capturedRequestSessionTokenList.get(3*i+0)).isNotEmpty(); + + clientUnderTest.readDocument(documentCreated.getSelfLink(), null).toBlocking().single(); + + assertThat(capturedRequestSessionTokenList).hasSize(3*i+2); + assertThat(capturedRequestSessionTokenList.get(3*i+1)).isNotEmpty(); + + clientUnderTest.readDocument(documentCreated.getSelfLink(), null).toBlocking().single(); + + assertThat(capturedRequestSessionTokenList).hasSize(3*i+3); + assertThat(capturedRequestSessionTokenList.get(3*i+2)).isNotEmpty(); + } + } + + private void setupSpySession(final List capturedRequestSessionTokenList, final List capturedResponseSessionTokenList, + RxDocumentClientImpl spyClient, final RxDocumentClientImpl origClient) throws DocumentClientException { + + Mockito.reset(spyClient); + doAnswer(new Answer() { + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + RxDocumentServiceRequest req = (RxDocumentServiceRequest) args[0]; + DocumentServiceResponse resp = (DocumentServiceResponse) args[1]; + + capturedRequestSessionTokenList.add(req.getHeaders().get(HttpConstants.HttpHeaders.SESSION_TOKEN)); + capturedResponseSessionTokenList.add(resp.getResponseHeaders().get(HttpConstants.HttpHeaders.SESSION_TOKEN)); + + origClient.captureSessionToken(req, resp); + + return null; + }}) + .when(spyClient).captureSessionToken(Mockito.any(RxDocumentServiceRequest.class), Mockito.any(DocumentServiceResponse.class)); + } + +} diff --git a/azure-documentdb-rx/src/test/resources/log4j.properties b/azure-documentdb-rx/src/test/resources/log4j.properties new file mode 100644 index 000000000000..fb890be40b27 --- /dev/null +++ b/azure-documentdb-rx/src/test/resources/log4j.properties @@ -0,0 +1,22 @@ +# this is the log4j configuration for tests + +# Set root logger level to DEBUG and its only appender to A1. +log4j.rootLogger=INFO, A1 + +# Set HTTP components' logger to INFO +#log4j.category.org.apache.http=TRACE +#log4j.category.org.apache.http.wire=TRACE +#log4j.category.org.apache.http.headers=TRACE +log4j.category.com.microsoft.azure.documentdb.internal.ServiceJNIWrapper=ERROR + +log4j.category.httpclient.wire=INFO + +log4j.category.org.apache.commons.httpclient=INFO +log4j.category.io.netty=INFO +log4j.category.io.reactivex=INFO +# A1 is set to be a ConsoleAppender. +log4j.appender.A1=org.apache.log4j.ConsoleAppender + +# A1 uses PatternLayout. +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%d %5X{pid} [%t] %-5p %c - %m%n \ No newline at end of file