SLING-7169 - Add IndexingClient with waitForAsyncIndexing
diff --git a/src/main/java/org/apache/sling/testing/clients/indexing/IndexingClient.java b/src/main/java/org/apache/sling/testing/clients/indexing/IndexingClient.java
new file mode 100644
index 0000000..7808891
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/indexing/IndexingClient.java
@@ -0,0 +1,424 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * 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 org.apache.sling.testing.clients.indexing;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.apache.sling.testing.clients.SlingClientConfig;
+import org.apache.sling.testing.clients.osgi.OsgiConsoleClient;
+import org.apache.sling.testing.clients.query.QueryClient;
+import org.apache.sling.testing.clients.util.poller.Polling;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static java.util.UUID.randomUUID;
+import static org.apache.http.HttpStatus.SC_OK;
+
+/**
+ * <p>Interface to the oak indexing mechanism</p>
+ *
+ * <p>Exposes {@link #waitForAsyncIndexing(long, long)} for waiting all the indexing lanes to finish
+ * indexing and to guarantee all the indices are up to date</p>
+ *
+ * <p>For using {@link #waitForAsyncIndexing(long, long)}, the user must have access rights to:
+ * <ul>
+ * <li>read/write in {@code /tmp}</li>
+ * <li>install bundles via {@link org.apache.sling.testing.clients.osgi.OsgiConsoleClient}
+ * (if the query servlet was not previously installed)</li>
+ * </ul>
+ * In short, it requires administrative rights.
+ * </p>
+ */
+public class IndexingClient extends SlingClient {
+ private static final Logger LOG = LoggerFactory.getLogger(IndexingClient.class);
+
+ /** Root of all the data created by this tool. Its presence marks that it was already installed */
+ private static final String WAIT_FOR_ASYNC_INDEXING_ROOT = "/tmp/testing/waitForAsyncIndexing";
+
+ /** Where new index definitions are added */
+ private static final String INDEX_PATH = WAIT_FOR_ASYNC_INDEXING_ROOT + "/oak:index";
+
+ /** Where the content to be indexed is created */
+ private static final String CONTENT_PATH = WAIT_FOR_ASYNC_INDEXING_ROOT + "/content";
+
+ /** Prefix to be added to all the index names */
+ private static final String INDEX_PREFIX = "testIndexingLane-";
+
+ /** Prefix to be added to all the properties */
+ private static final String PROPERTY_PREFIX = "testProp-";
+
+ /** Prefix to be added to all the property values */
+ private static final String VALUE_PREFIX = "testasyncval-";
+
+ /** Prefix to be added to all the tags */
+ private static final String TAG_PREFIX = "testTag";
+
+ /** Placeholder for index name */
+ private static final String INDEX_NAME_PLACEHOLDER = "<<INDEXNAME>>";
+
+ /** Placeholder for random, unique parts in content and queries */
+ private static final String PROPERTY_PLACEHOLDER = "<<PROPNAME>>";
+
+ /** Placeholder for random, unique parts in content and queries */
+ private static final String VALUE_PLACEHOLDER = "<<RANDVAL>>";
+
+ /** Placeholder for identifying the lane to which the index and the content belongs to */
+ private static final String LANE_PLACEHOLDER = "<<LANE>>";
+
+ /** Placeholder for identifying the tag to which the index and the queries belongs to */
+ private static final String TAG_PLACEHOLDER = "<<TAG>>";
+
+ /** Template for index definitions to be installed */
+ private static final String INDEX_DEFINITION = "{" +
+ " '" + INDEX_NAME_PLACEHOLDER + "': {\n" +
+ " 'jcr:primaryType': 'oak:QueryIndexDefinition',\n" +
+ " 'type': 'lucene',\n" +
+ " 'async': '" + LANE_PLACEHOLDER + "',\n" +
+ " 'tags': '" + TAG_PLACEHOLDER + "',\n" +
+ " 'indexRules': {\n" +
+ " 'jcr:primaryType': 'nt:unstructured',\n" +
+ " 'nt:base': {\n" +
+ " 'jcr:primaryType': 'nt:unstructured',\n" +
+ " 'properties': {\n" +
+ " 'jcr:primaryType': 'nt:unstructured',\n" +
+ " '" + PROPERTY_PLACEHOLDER + "': {\n" +
+ " 'jcr:primaryType': 'nt:unstructured',\n" +
+ " 'name': '" + PROPERTY_PLACEHOLDER + "',\n" +
+ " 'analyzed': true\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " }\n" +
+ " }" +
+ "}";
+
+ /** Template for the content to be created and searched */
+ private static final String CONTENT_DEFINITION = "{" +
+ "'testContent-" + LANE_PLACEHOLDER + "-" + VALUE_PLACEHOLDER + "': {" +
+ " 'jcr:primaryType': 'nt:unstructured', " +
+ " '" + PROPERTY_PLACEHOLDER +"': '" + VALUE_PLACEHOLDER + "'" +
+ "}}";
+
+
+ /** Templates for queries to be executed against each index, in order of priority */
+ private static final List<String> QUERIES = Arrays.asList(
+ // for Oak versions that support option(index tag testTag)
+ "/jcr:root" + WAIT_FOR_ASYNC_INDEXING_ROOT + "//*" +
+ "[jcr:contains(@" + PROPERTY_PLACEHOLDER + ", '" + VALUE_PLACEHOLDER +"')] " +
+ "option(traversal ok, index tag " + TAG_PLACEHOLDER + ")",
+ // for older Oak versions
+ "/jcr:root" + WAIT_FOR_ASYNC_INDEXING_ROOT + "//*" +
+ "[jcr:contains(@" + PROPERTY_PLACEHOLDER + ", '" + VALUE_PLACEHOLDER +"')] " +
+ "option(traversal ok)"
+ );
+
+ /** Global counter for how much time was spent in total waiting for async indexing */
+ private static final AtomicLong totalWaited = new AtomicLong();
+
+ /**
+ * Constructor used by Builders and adaptTo(). <b>Should never be called directly from the code.</b>
+ *
+ * @param http the underlying HttpClient to be used
+ * @param config sling specific configs
+ * @throws ClientException if the client could not be created
+ */
+ public IndexingClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ super(http, config);
+ }
+
+ /**
+ * <p>Handy constructor easy to use in simple tests. Creates a client that uses basic authentication.</p>
+ *
+ * <p>For constructing clients with complex configurations, use a {@link InternalBuilder}</p>
+ *
+ * <p>For constructing clients with the same configuration, but a different class, use {@link #adaptTo(Class)}</p>
+ *
+ * @param url url of the server (including context path)
+ * @param user username for basic authentication
+ * @param password password for basic authentication
+ * @throws ClientException never, kept for uniformity with the other constructors
+ */
+ public IndexingClient(URI url, String user, String password) throws ClientException {
+ super(url, user, password);
+ }
+
+ /**
+ * Retrieves the list of indexing lanes configured on the instance
+ *
+ * @return list of lane names
+ * @throws ClientException if the request fails
+ */
+ public List<String> getLaneNames() throws ClientException {
+ try {
+ Object asyncConfigs = adaptTo(OsgiConsoleClient.class)
+ .getConfiguration("org.apache.jackrabbit.oak.plugins.index.AsyncIndexerService")
+ .get("asyncConfigs");
+
+ if (asyncConfigs instanceof String[]) {
+ String[] configs = (String[]) asyncConfigs; // ugly, we should refactor OsgiConsoleClient
+
+ List<String> lanes = new ArrayList<>(configs.length);
+ for (String asyncConfig : configs) {
+ lanes.add(asyncConfig.split(":")[0]);
+ }
+ return lanes;
+ } else {
+ throw new ClientException("Cannot retrieve config from AsyncIndexerService, asyncConfigs is not a String[]");
+ }
+ } catch (Exception e) {
+ throw new ClientException("Failed to retrieve lanes", e);
+ }
+ }
+
+ /**
+ * <p>Blocks until all the async indices are up to date, to guarantee that the susequent queries return
+ * all the results.</p>
+ *
+ * <p>Works by creating a custom index for each lane, adding specific content to
+ * be indexed by these indices and then repeatedly searching this content until everything is found (indexed).
+ * All the content is created under {@value #WAIT_FOR_ASYNC_INDEXING_ROOT}</p>
+ *
+ * <p>Indices are automatically created, but only if not already present.
+ * This method does not delete the indices at the end to avoid generating too much noise on the instance.
+ * To completely clean any traces, the user must call {@link #uninstall()}</p>
+ *
+ * <p>Requires administrative rights to install bundles and to create nodes under
+ * {@value #WAIT_FOR_ASYNC_INDEXING_ROOT}</p>
+ *
+ * @param timeout max time to wait, in milliseconds, before throwing {@code TimeoutException}
+ * @param delay time to sleep between retries
+ * @throws TimeoutException if the {@code timeout} was reached before all the indices were updated
+ * @throws InterruptedException to mark this method as waiting
+ * @throws ClientException if an error occurs during http requests/responses
+ */
+ public void waitForAsyncIndexing(final long timeout, final long delay)
+ throws TimeoutException, InterruptedException, ClientException {
+
+ install(); // will install only if needed
+
+ final String uniqueValue = randomUUID().toString(); // to be added in all the content nodes
+ final List<String> lanes = getLaneNames(); // dynamically detect which lanes to wait for
+
+ Polling p = new Polling(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return searchContent(lanes, uniqueValue);
+ }
+ });
+
+ try {
+ createContent(lanes, uniqueValue);
+ p.poll(timeout, delay);
+ } finally {
+ long total = totalWaited.addAndGet(p.getWaited()); // count waited in all the cases (timeout)
+ LOG.info("Waited for async index {} ms (overall: {} ms)", p.getWaited(), total);
+ try {
+ deleteContent(uniqueValue);
+ } catch (ClientException e) {
+ LOG.warn("Failed to delete temporary content", e);
+ }
+ }
+ }
+
+ /**
+ * Same as {@link #waitForAsyncIndexing(long timeout, long delay)},
+ * but with default values for {@code timeout=1min} and {@code delay=500ms}.
+ *
+ * @see #waitForAsyncIndexing(long, long)
+ *
+ * @throws TimeoutException if the {@code timeout} was reached before all the indices were updated
+ * @throws InterruptedException to mark this method as waiting
+ * @throws ClientException if an error occurs during http requests/responses
+ */
+ public void waitForAsyncIndexing() throws InterruptedException, ClientException, TimeoutException {
+ waitForAsyncIndexing(TimeUnit.MINUTES.toMillis(1), 500);
+ }
+
+ /**
+ * <p>Creates the necessary custom indices in the repository, if not already present.</p>
+ *
+ * <p>It is automatically called in each wait, there's no need to
+ * explicitly invoke it from the test.</p>
+ *
+ * @throws ClientException if the installation fails
+ */
+ public void install() throws ClientException {
+ if (exists(WAIT_FOR_ASYNC_INDEXING_ROOT)) {
+ LOG.debug("Skipping install since {} already exists", WAIT_FOR_ASYNC_INDEXING_ROOT);
+ return;
+ }
+
+ createNodeRecursive(WAIT_FOR_ASYNC_INDEXING_ROOT, "sling:Folder");
+ createNode(INDEX_PATH, "nt:unstructured");
+ createNode(CONTENT_PATH, "sling:Folder");
+
+ final List<String> lanes = getLaneNames();
+ for (String lane : lanes) {
+ String indexName = getIndexName(lane);
+ String indexDefinition = replacePlaceholders(INDEX_DEFINITION, lane, null);
+ LOG.info("Creating index {} in {}", indexName, INDEX_PATH);
+ LOG.debug(indexDefinition);
+ importContent(INDEX_PATH, "json", indexDefinition);
+ // Trigger reindex to make sure the complete index definition is used
+ setPropertyString(INDEX_PATH + "/" + indexName, "reindex", "true");
+ }
+ }
+
+ /**
+ * <p>Cleans all the data generated by {@link #install()} and {@link #waitForAsyncIndexing(long, long)}.</p>
+ *
+ * <p>User must manually call this if needed, as opposed to {@link #install()}, which is called
+ * automatically.</p>
+ *
+ * @throws ClientException if the cleanup failed
+ */
+ public void uninstall() throws ClientException {
+ deletePath(WAIT_FOR_ASYNC_INDEXING_ROOT, SC_OK);
+ }
+
+ /**
+ * Creates all the content structures to be indexed, one for each lane,
+ * with the given {@code uniqueValue}, to make them easily identifiable
+ *
+ * @param lanes list of lanes for which to create the content
+ * @param uniqueValue the unique value to be added
+ * @throws ClientException if the content creation fails
+ */
+ private void createContent(final List<String> lanes, final String uniqueValue) throws ClientException {
+ // All the content is grouped under the same node
+ String contentHolder = CONTENT_PATH + "/" + uniqueValue;
+ LOG.debug("creating content in {}", contentHolder);
+ createNode(contentHolder, "sling:Folder");
+
+ for (String lane : lanes) {
+ String contentNode = replacePlaceholders(CONTENT_DEFINITION, lane, uniqueValue);
+ LOG.debug("creating: {}", contentNode);
+ importContent(contentHolder, "json", contentNode);
+ }
+ }
+
+ /**
+ * Deletes the temporary nodes created in {@link #createContent(List, String)}
+ *
+ * @throws ClientException if the content cannot be deleted
+ */
+ private void deleteContent(String uniqueValue) throws ClientException {
+ if (uniqueValue != null) {
+ String contentHolder = CONTENT_PATH + "/" + uniqueValue;
+ LOG.debug("deleting {}", contentHolder);
+ deletePath(contentHolder, SC_OK);
+ }
+ }
+
+ /**
+ * Performs queries for each of the created content and checks that all return results
+ *
+ * @param lanes list of lanes for which to run queries
+ * @param uniqueValue the unique value to be used in queries
+ * @return true if all the queries returned at least one result (all indices are up to date)
+ * @throws ClientException if the http request failed
+ * @throws InterruptedException to mark this method as waiting
+ */
+ private boolean searchContent(final List<String> lanes, final String uniqueValue)
+ throws ClientException, InterruptedException {
+ for (String lane : lanes) {
+ if (!searchContentForIndex(lane, uniqueValue)) {
+ return false;
+ }
+ }
+ // Queries returned at least one result for each index
+ return true;
+ }
+
+ /**
+ * Tries all the known queries for a specific index lane,
+ * until one of them returns at least one result.
+ *
+ * @param lane the indexing lane to query
+ * @param uniqueValue the unique value to be used in queries
+ * @return true if at least one query returned results
+ * @throws ClientException if the http request fails
+ * @throws InterruptedException to mark this method as waiting
+ */
+ private boolean searchContentForIndex(final String lane, final String uniqueValue)
+ throws ClientException, InterruptedException {
+ QueryClient queryClient = adaptTo(QueryClient.class);
+
+ for (String query : QUERIES) {
+ // prepare the query with the final values
+ String indexName = getIndexName(lane);
+ String effectiveQuery = replacePlaceholders(query, lane, uniqueValue);
+
+ try {
+ // Check query plan to make sure we use the good index
+ String plan = queryClient.getPlan(effectiveQuery, QueryClient.QueryType.XPATH);
+ if (plan.contains(indexName)) {
+ // The proper index is used, we can check the results
+ long results = queryClient.doCount(effectiveQuery, QueryClient.QueryType.XPATH);
+ if (results > 0) {
+ LOG.debug("Found {} results using query {}", results, effectiveQuery);
+ return true;
+ }
+ } else {
+ LOG.debug("Did not find index {} in plan: {}", indexName, plan);
+ LOG.debug("Will try the next query, if available");
+ }
+ } catch (ClientException e) {
+ if (e.getHttpStatusCode() == 400) {
+ LOG.debug("Unsupported query: {}", effectiveQuery);
+ LOG.debug("Will try the next query, if available");
+ } else {
+ // We don't continue if there's another problem
+ throw e;
+ }
+ }
+ }
+ // No query returned results
+ return false;
+ }
+
+ private String replacePlaceholders(String original, String lane, String value) {
+ // Tags must be alphanumeric
+ String tag = StringUtils.capitalize(lane.replaceAll("[^A-Za-z0-9]", ""));
+
+ String result = original;
+ result = StringUtils.replace(result, LANE_PLACEHOLDER, lane);
+ result = StringUtils.replace(result, INDEX_NAME_PLACEHOLDER, INDEX_PREFIX + lane);
+ result = StringUtils.replace(result, PROPERTY_PLACEHOLDER, PROPERTY_PREFIX + lane);
+ result = StringUtils.replace(result, VALUE_PLACEHOLDER, VALUE_PREFIX + value);
+ result = StringUtils.replace(result, TAG_PLACEHOLDER, TAG_PREFIX + tag);
+
+ return result;
+ }
+
+ private String getIndexName(final String lane) {
+ return INDEX_PREFIX + lane;
+ }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/indexing/package-info.java b/src/main/java/org/apache/sling/testing/clients/indexing/package-info.java
new file mode 100644
index 0000000..385a674
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/indexing/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+@Version("0.1.0")
+package org.apache.sling.testing.clients.indexing;
+
+import org.osgi.annotation.versioning.Version;
diff --git a/src/test/java/org/apache/sling/testing/clients/indexing/IndexingClientTest.java b/src/test/java/org/apache/sling/testing/clients/indexing/IndexingClientTest.java
new file mode 100644
index 0000000..a509a21
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/clients/indexing/IndexingClientTest.java
@@ -0,0 +1,196 @@
+/*******************************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * 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 org.apache.sling.testing.clients.indexing;
+
+import org.apache.http.*;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHttpEntityEnclosingRequest;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.HttpServerRule;
+import org.apache.sling.testing.clients.query.servlet.QueryServlet;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+public class IndexingClientTest {
+ private static final Logger LOG = LoggerFactory.getLogger(IndexingClientTest.class);
+
+ private static final String EXPLAIN_RESPONSE = "{\"plan\": \"random plan with testIndexingLane-async and testIndexingLane-fulltext-async\",\"time\": 1}";
+ private static final String QUERY_RESPONSE = "{\"total\": 1234,\"time\": 1}";
+
+ @ClassRule
+ public static HttpServerRule httpServer = new HttpServerRule() {
+ HttpRequestHandler okHandler = new HttpRequestHandler() {
+ @Override
+ public void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ response.setStatusCode(200);
+ response.setEntity(new StringEntity("Everything's fine"));
+ }
+ };
+
+ HttpRequestHandler createdHandler = new HttpRequestHandler() {
+ @Override
+ public void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ response.setStatusCode(201);
+ response.setEntity(new StringEntity("Created"));
+ }
+ };
+
+ @Override
+ protected void registerHandlers() throws IOException {
+ // Normal query request
+ serverBootstrap.registerHandler(QueryServlet.SERVLET_PATH, new HttpRequestHandler() {
+ @Override
+ public void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ List<NameValuePair> parameters = URLEncodedUtils.parse(
+ request.getRequestLine().getUri(), Charset.defaultCharset());
+
+ for (NameValuePair parameter : parameters) {
+ if (parameter.getName().equals("explain") && !parameter.getValue().equals("false")) {
+ response.setEntity(new StringEntity(EXPLAIN_RESPONSE));
+ return;
+ }
+ }
+
+ response.setEntity(new StringEntity(QUERY_RESPONSE));
+ }
+ });
+
+ // Install servlet
+ serverBootstrap.registerHandler("/system/console/bundles", new HttpRequestHandler() {
+ @Override
+ public void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ // is install (post) or checking status (get)
+ if (request instanceof BasicHttpEntityEnclosingRequest) {
+ response.setStatusCode(302);
+ } else {
+ response.setStatusCode(200);
+ }
+ }
+ });
+
+ // Check bundle status
+ serverBootstrap.registerHandler("BUNDLE_PATH" + ".json", new HttpRequestHandler() {
+ @Override
+ public void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ response.setEntity(new StringEntity("JSON_BUNDLE"));
+ }
+ });
+
+ // Uninstall bundle
+ serverBootstrap.registerHandler("BUNDLE_PATH", new HttpRequestHandler() {
+ @Override
+ public void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ response.setStatusCode(200);
+ }
+ });
+
+ // Uninstall bundle
+ serverBootstrap.registerHandler(
+ "/system/console/configMgr/org.apache.jackrabbit.oak.plugins.index.AsyncIndexerService",
+ new HttpRequestHandler() {
+ @Override
+ public void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ response.setStatusCode(200);
+ response.setEntity(new StringEntity("{\"properties\":{" +
+ "\"asyncConfigs\":{\"values\":[\"async:5\",\"fulltext-async:5\"]}}}"));
+ }
+ }
+ );
+
+ serverBootstrap.registerHandler("/tmp/testing/waitForAsyncIndexing/content/*", new HttpRequestHandler() {
+ @Override
+ public void handle(HttpRequest request, HttpResponse response, HttpContext context)
+ throws HttpException, IOException {
+ List<NameValuePair> params = extractParameters(request);
+
+ for (NameValuePair param : params) {
+ if (param.getName().equals(":operation") && (param.getValue().equals("delete"))) {
+ response.setStatusCode(200);
+ return;
+ }
+ }
+
+ response.setStatusCode(201);
+ response.setEntity(new StringEntity("Created!"));
+ }
+ });
+
+ // unimportant requests
+ serverBootstrap.registerHandler("/tmp.json", okHandler);
+ serverBootstrap.registerHandler("/tmp/testing.json", okHandler);
+ serverBootstrap.registerHandler("/tmp/testing/waitForAsyncIndexing", okHandler);
+ serverBootstrap.registerHandler("/tmp/testing", okHandler);
+ serverBootstrap.registerHandler("/tmp/testing/waitForAsyncIndexing/oak:index", createdHandler);
+ serverBootstrap.registerHandler("/tmp/testing/waitForAsyncIndexing/content", createdHandler);
+ }
+ };
+
+ private IndexingClient client;
+
+ public IndexingClientTest() throws ClientException {
+ client = new IndexingClient(httpServer.getURI(), "admin", "admin");
+ client = new IndexingClient(java.net.URI.create("http://localhost:4502"), "admin", "admin");
+ }
+
+ @Test
+ public void testInstall() throws ClientException {
+ client.install();
+ }
+
+ @Test
+ public void testUninstall() throws ClientException {
+ client.uninstall();
+ }
+
+ @Test
+ public void testWaitForAsyncIndexing() throws ClientException, TimeoutException, InterruptedException {
+ client.waitForAsyncIndexing();
+ }
+
+ private static List<NameValuePair> extractParameters(HttpRequest request) {
+ if (request instanceof HttpEntityEnclosingRequest) {
+ HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
+ try {
+ return URLEncodedUtils.parse(entity);
+ } catch (IOException e) {
+ LOG.error("Failed to parse entity", e);
+ }
+ }
+
+ return new ArrayList<>();
+ }
+}