diff --git a/.travis.yml b/.travis.yml
index 1e703899bbe..69fb0cd9adb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,6 +15,10 @@
language: java
jdk:
- oraclejdk8
+addons:
+ apt:
+ packages:
+ - expect
before_install:
- openssl aes-256-cbc -K $encrypted_37a4f399de75_key -iv $encrypted_37a4f399de75_iv -in service-account.json.enc -out service-account.json -d
&& export GOOGLE_APPLICATION_CREDENTIALS=$TRAVIS_BUILD_DIR/service-account.json GCLOUD_PROJECT=cloud-samples-tests
diff --git a/appengine/datastore/indexes-exploding/pom.xml b/appengine/datastore/indexes-exploding/pom.xml
new file mode 100644
index 00000000000..37a2a05154b
--- /dev/null
+++ b/appengine/datastore/indexes-exploding/pom.xml
@@ -0,0 +1,91 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-datastore-indexes-exploding
+
+ com.google.cloud
+ doc-samples
+ 1.0.0
+ ../../..
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+ javax.servlet
+ servlet-api
+ jar
+ provided
+
+
+
+
+ junit
+ junit
+ 4.10
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.28
+ test
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+ com.google.appengine
+ appengine-maven-plugin
+ ${appengine.sdk.version}
+
+
+
+
diff --git a/appengine/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java b/appengine/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java
new file mode 100644
index 00000000000..dff4396b797
--- /dev/null
+++ b/appengine/datastore/indexes-exploding/src/main/java/com/example/appengine/IndexesServlet.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A servlet to demonstrate the use of Cloud Datastore indexes.
+ */
+public class IndexesServlet extends HttpServlet {
+ private final DatastoreService datastore;
+
+ public IndexesServlet() {
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ Query q =
+ new Query("Widget")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("x", FilterOperator.EQUAL, 1),
+ new FilterPredicate("y", FilterOperator.EQUAL, "red")))
+ .addSort("date", Query.SortDirection.ASCENDING);
+ List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());
+
+ PrintWriter out = resp.getWriter();
+ out.printf("Got %d widgets.\n", results.size());
+ }
+}
diff --git a/appengine/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..e9d8b21cb8f
--- /dev/null
+++ b/appengine/datastore/indexes-exploding/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,21 @@
+
+
+
+ YOUR-PROJECT-ID
+ YOUR-VERSION-ID
+ true
+
diff --git a/appengine/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 00000000000..e0f85d3812d
--- /dev/null
+++ b/appengine/datastore/indexes-exploding/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appengine/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml b/appengine/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..049fd7a05e7
--- /dev/null
+++ b/appengine/datastore/indexes-exploding/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ indexes-servlet
+ com.example.appengine.IndexesServlet
+
+
+ indexes-servlet
+ /
+
+
+
+
+ profile
+ /*
+
+
+ CONFIDENTIAL
+
+
+
diff --git a/appengine/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java b/appengine/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java
new file mode 100644
index 00000000000..451f2189d34
--- /dev/null
+++ b/appengine/datastore/indexes-exploding/src/test/java/com/example/appengine/IndexesServletTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Date;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link IndexesServlet}.
+ */
+@RunWith(JUnit4.class)
+public class IndexesServletTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private IndexesServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new IndexesServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_emptyDatastore_writesNoWidgets() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ assertThat(responseWriter.toString())
+ .named("IndexesServlet response")
+ .isEqualTo("Got 0 widgets.\n");
+ }
+
+ @Test
+ public void doGet_repeatedPropertyEntities_writesWidgets() throws Exception {
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+ // [START exploding_index_example_3]
+ Entity widget = new Entity("Widget");
+ widget.setProperty("x", Arrays.asList(1, 2, 3, 4));
+ widget.setProperty("y", Arrays.asList("red", "green", "blue"));
+ widget.setProperty("date", new Date());
+ datastore.put(widget);
+ // [END exploding_index_example_3]
+
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ assertThat(responseWriter.toString())
+ .named("IndexesServlet response")
+ .isEqualTo("Got 1 widgets.\n");
+ }
+}
diff --git a/appengine/datastore/indexes-perfect/pom.xml b/appengine/datastore/indexes-perfect/pom.xml
new file mode 100644
index 00000000000..14fab365685
--- /dev/null
+++ b/appengine/datastore/indexes-perfect/pom.xml
@@ -0,0 +1,91 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-datastore-indexes-perfect
+
+ com.google.cloud
+ doc-samples
+ 1.0.0
+ ../../..
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+ javax.servlet
+ servlet-api
+ jar
+ provided
+
+
+
+
+ junit
+ junit
+ 4.10
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.28
+ test
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+ com.google.appengine
+ appengine-maven-plugin
+ ${appengine.sdk.version}
+
+
+
+
diff --git a/appengine/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java b/appengine/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java
new file mode 100644
index 00000000000..0dbca3ce307
--- /dev/null
+++ b/appengine/datastore/indexes-perfect/src/main/java/com/example/appengine/IndexesServlet.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A servlet to demonstrate the use of Cloud Datastore indexes.
+ */
+public class IndexesServlet extends HttpServlet {
+ private final DatastoreService datastore;
+
+ public IndexesServlet() {
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ PrintWriter out = resp.getWriter();
+ // These queries should all work with the same index.
+ // [START queries_and_indexes_example_1]
+ Query q1 =
+ new Query("Person")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("lastName", FilterOperator.EQUAL, "Smith"),
+ new FilterPredicate("height", FilterOperator.EQUAL, 72)))
+ .addSort("height", Query.SortDirection.DESCENDING);
+ // [END queries_and_indexes_example_1]
+ List r1 = datastore.prepare(q1).asList(FetchOptions.Builder.withDefaults());
+ out.printf("Got %d results from query 1.\n", r1.size());
+
+ // [START queries_and_indexes_example_2]
+ Query q2 =
+ new Query("Person")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("lastName", FilterOperator.EQUAL, "Jones"),
+ new FilterPredicate("height", FilterOperator.EQUAL, 63)))
+ .addSort("height", Query.SortDirection.DESCENDING);
+ // [END queries_and_indexes_example_2]
+ List r2 = datastore.prepare(q2).asList(FetchOptions.Builder.withDefaults());
+ out.printf("Got %d results from query 2.\n", r2.size());
+
+ // [START queries_and_indexes_example_3]
+ Query q3 =
+ new Query("Person")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("lastName", FilterOperator.EQUAL, "Friedkin"),
+ new FilterPredicate("firstName", FilterOperator.EQUAL, "Damian")))
+ .addSort("height", Query.SortDirection.ASCENDING);
+ // [END queries_and_indexes_example_3]
+ List r3 = datastore.prepare(q3).asList(FetchOptions.Builder.withDefaults());
+ out.printf("Got %d results from query 3.\n", r3.size());
+
+ // [START queries_and_indexes_example_4]
+ Query q4 =
+ new Query("Person")
+ .setFilter(new FilterPredicate("lastName", FilterOperator.EQUAL, "Blair"))
+ .addSort("firstName", Query.SortDirection.ASCENDING)
+ .addSort("height", Query.SortDirection.ASCENDING);
+ // [END queries_and_indexes_example_4]
+ List r4 = datastore.prepare(q4).asList(FetchOptions.Builder.withDefaults());
+ out.printf("Got %d results from query 4.\n", r4.size());
+ }
+}
diff --git a/appengine/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..e9d8b21cb8f
--- /dev/null
+++ b/appengine/datastore/indexes-perfect/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,21 @@
+
+
+
+ YOUR-PROJECT-ID
+ YOUR-VERSION-ID
+ true
+
diff --git a/appengine/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 00000000000..bb56fb4bf50
--- /dev/null
+++ b/appengine/datastore/indexes-perfect/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appengine/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml b/appengine/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..049fd7a05e7
--- /dev/null
+++ b/appengine/datastore/indexes-perfect/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ indexes-servlet
+ com.example.appengine.IndexesServlet
+
+
+ indexes-servlet
+ /
+
+
+
+
+ profile
+ /*
+
+
+ CONFIDENTIAL
+
+
+
diff --git a/appengine/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java b/appengine/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java
new file mode 100644
index 00000000000..e82f5b302ab
--- /dev/null
+++ b/appengine/datastore/indexes-perfect/src/test/java/com/example/appengine/IndexesServletTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link IndexesServlet}.
+ */
+@RunWith(JUnit4.class)
+public class IndexesServletTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private IndexesServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new IndexesServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_emptyDatastore_writesNoWidgets() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ String response = responseWriter.toString();
+ assertThat(response).contains("Got 0 results from query 1.");
+ assertThat(response).contains("Got 0 results from query 2.");
+ assertThat(response).contains("Got 0 results from query 3.");
+ assertThat(response).contains("Got 0 results from query 4.");
+ }
+}
diff --git a/appengine/datastore/indexes/pom.xml b/appengine/datastore/indexes/pom.xml
new file mode 100644
index 00000000000..b6c95b9a3dd
--- /dev/null
+++ b/appengine/datastore/indexes/pom.xml
@@ -0,0 +1,91 @@
+
+
+ 4.0.0
+ war
+ 1.0-SNAPSHOT
+ com.example.appengine
+ appengine-datastore-indexes
+
+ com.google.cloud
+ doc-samples
+ 1.0.0
+ ../../..
+
+
+
+ com.google.appengine
+ appengine-api-1.0-sdk
+ ${appengine.sdk.version}
+
+
+ javax.servlet
+ servlet-api
+ jar
+ provided
+
+
+
+
+ junit
+ junit
+ 4.10
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.10.19
+ test
+
+
+ com.google.appengine
+ appengine-testing
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-api-stubs
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.appengine
+ appengine-tools-sdk
+ ${appengine.sdk.version}
+ test
+
+
+ com.google.truth
+ truth
+ 0.28
+ test
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}/WEB-INF/classes
+
+
+
+ com.google.appengine
+ appengine-maven-plugin
+ ${appengine.sdk.version}
+
+
+
+
diff --git a/appengine/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java b/appengine/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java
new file mode 100644
index 00000000000..78605f64bf2
--- /dev/null
+++ b/appengine/datastore/indexes/src/main/java/com/example/appengine/IndexesServlet.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.CompositeFilterOperator;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A servlet to demonstrate the use of Cloud Datastore indexes.
+ */
+public class IndexesServlet extends HttpServlet {
+ private final DatastoreService datastore;
+
+ public IndexesServlet() {
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws IOException, ServletException {
+ // [START exploding_index_example_1]
+ Query q =
+ new Query("Widget")
+ .setFilter(
+ CompositeFilterOperator.and(
+ new FilterPredicate("x", FilterOperator.EQUAL, 1),
+ new FilterPredicate("y", FilterOperator.EQUAL, 2)))
+ .addSort("date", Query.SortDirection.ASCENDING);
+ // [END exploding_index_example_1]
+ List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());
+
+ PrintWriter out = resp.getWriter();
+ out.printf("Got %d widgets.\n", results.size());
+ }
+}
diff --git a/appengine/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml b/appengine/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml
new file mode 100644
index 00000000000..e9d8b21cb8f
--- /dev/null
+++ b/appengine/datastore/indexes/src/main/webapp/WEB-INF/appengine-web.xml
@@ -0,0 +1,21 @@
+
+
+
+ YOUR-PROJECT-ID
+ YOUR-VERSION-ID
+ true
+
diff --git a/appengine/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml b/appengine/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml
new file mode 100644
index 00000000000..106de35043d
--- /dev/null
+++ b/appengine/datastore/indexes/src/main/webapp/WEB-INF/datastore-indexes.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/appengine/datastore/indexes/src/main/webapp/WEB-INF/web.xml b/appengine/datastore/indexes/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 00000000000..049fd7a05e7
--- /dev/null
+++ b/appengine/datastore/indexes/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ indexes-servlet
+ com.example.appengine.IndexesServlet
+
+
+ indexes-servlet
+ /
+
+
+
+
+ profile
+ /*
+
+
+ CONFIDENTIAL
+
+
+
diff --git a/appengine/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java b/appengine/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java
new file mode 100644
index 00000000000..daf6e8f3821
--- /dev/null
+++ b/appengine/datastore/indexes/src/test/java/com/example/appengine/IndexesServletTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Unit tests for {@link IndexesServlet}.
+ */
+@RunWith(JUnit4.class)
+public class IndexesServletTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
+
+ @Mock private HttpServletRequest mockRequest;
+ @Mock private HttpServletResponse mockResponse;
+ private StringWriter responseWriter;
+ private IndexesServlet servletUnderTest;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ helper.setUp();
+
+ // Set up a fake HTTP response.
+ responseWriter = new StringWriter();
+ when(mockResponse.getWriter()).thenReturn(new PrintWriter(responseWriter));
+
+ servletUnderTest = new IndexesServlet();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void doGet_emptyDatastore_writesNoWidgets() throws Exception {
+ servletUnderTest.doGet(mockRequest, mockResponse);
+
+ assertThat(responseWriter.toString())
+ .named("IndexesServlet response")
+ .isEqualTo("Got 0 widgets.\n");
+ }
+}
diff --git a/appengine/datastore/src/test/java/com/example/appengine/IndexesTest.java b/appengine/datastore/src/test/java/com/example/appengine/IndexesTest.java
new file mode 100644
index 00000000000..6a3e616bb5a
--- /dev/null
+++ b/appengine/datastore/src/test/java/com/example/appengine/IndexesTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.appengine;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.FetchOptions;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.Filter;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.datastore.Query.FilterPredicate;
+import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
+import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
+import com.google.common.collect.ImmutableList;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+/**
+ * Unit tests to demonstrate App Engine Datastore queries.
+ */
+@RunWith(JUnit4.class)
+public class IndexesTest {
+
+ private final LocalServiceTestHelper helper =
+ new LocalServiceTestHelper(
+ // Set no eventual consistency, that way queries return all results.
+ // https://cloud.google.com/appengine/docs/java/tools/localunittesting#Java_Writing_High_Replication_Datastore_tests
+ new LocalDatastoreServiceTestConfig()
+ .setDefaultHighRepJobPolicyUnappliedJobPercentage(0));
+
+ private DatastoreService datastore;
+
+ @Before
+ public void setUp() {
+ helper.setUp();
+ datastore = DatastoreServiceFactory.getDatastoreService();
+ }
+
+ @After
+ public void tearDown() {
+ helper.tearDown();
+ }
+
+ @Test
+ public void propertyFilterExample_returnsMatchingEntities() throws Exception {
+ // [START unindexed_properties_1]
+ DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
+
+ Key acmeKey = KeyFactory.createKey("Company", "Acme");
+
+ Entity tom = new Entity("Person", "Tom", acmeKey);
+ tom.setProperty("name", "Tom");
+ tom.setProperty("age", 32);
+ datastore.put(tom);
+
+ Entity lucy = new Entity("Person", "Lucy", acmeKey);
+ lucy.setProperty("name", "Lucy");
+ lucy.setUnindexedProperty("age", 29);
+ datastore.put(lucy);
+
+ Filter ageFilter = new FilterPredicate("age", FilterOperator.GREATER_THAN, 25);
+
+ Query q = new Query("Person").setAncestor(acmeKey).setFilter(ageFilter);
+
+ // Returns tom but not lucy, because her age is unindexed
+ List results = datastore.prepare(q).asList(FetchOptions.Builder.withDefaults());
+ // [END unindexed_properties_1]
+
+ assertThat(getKeys(results)).named("query result keys").containsExactly(tom.getKey());
+ }
+
+ private ImmutableList getKeys(List entities) {
+ ImmutableList.Builder keys = ImmutableList.builder();
+ for (Entity entity : entities) {
+ keys.add(entity.getKey());
+ }
+ return keys.build();
+ }
+}
diff --git a/java-repo-tools/test-devserver.sh b/java-repo-tools/test-devserver.sh
new file mode 100755
index 00000000000..62b3dfefef7
--- /dev/null
+++ b/java-repo-tools/test-devserver.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Usage:
+# test-devserver.sh path/to/project
+#
+# This script runs the local appengine:devserver Maven plugin and verifies that
+# a request to http://localhost:8080/ does not return an error code.
+#
+# As an example, this is useful for verifying that datastore-indexes.xml is
+# correct (only if autoGenerate=false and the / handler does all queries used),
+# as an example.
+
+set -e
+set -x
+
+if [ -z "$1" ]; then
+ echo "Missing directory parameter."
+ echo "Usage:"
+ echo " $0 path/to/project"
+ exit 1
+fi
+
+(
+cd "$1"
+expect -c '
+ spawn mvn --batch-mode clean appengine:devserver -DskipTests
+ set timeout 600
+ expect localhost:8080
+ sleep 10
+ spawn curl --silent --output /dev/stderr --write-out "%{http_code}" http://localhost:8080/
+ expect {
+ "200" {
+ exit
+ }
+ }
+ exit 1
+ '
+)
+
diff --git a/pom.xml b/pom.xml
index e37c1554d2d..bd885080d87 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,9 @@
appengine/appidentity
appengine/channel
appengine/datastore
+ appengine/datastore/indexes
+ appengine/datastore/indexes-exploding
+ appengine/datastore/indexes-perfect
appengine/guestbook-objectify
appengine/helloworld
appengine/logs
diff --git a/travis.sh b/travis.sh
index fe0b680e9d9..7065ce69a80 100755
--- a/travis.sh
+++ b/travis.sh
@@ -24,3 +24,13 @@ if [ -z "$GOOGLE_APPLICATION_CREDENTIALS"]; then
fi
mvn --batch-mode clean verify -DskipTests=$SKIP_TESTS | egrep -v "(^\[INFO\] Download|^\[INFO\].*skipping)"
+# Run tests using App Engine local devserver.
+devserver_tests=(
+ appengine/datastore/indexes
+ appengine/datastore/indexes-exploding
+ appengine/datastore/indexes-perfect
+)
+for testdir in ${devserver_tests[@]} ; do
+ ./java-repo-tools/test-devserver.sh "${testdir}"
+done
+