You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2018/03/06 08:23:54 UTC

[sling-org-apache-sling-testing-clients] branch master updated: SLING-7509 - Add QueryClient

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-clients.git


The following commit(s) were added to refs/heads/master by this push:
     new 2988bbe  SLING-7509 - Add QueryClient
2988bbe is described below

commit 2988bbe2456c2cfd6beb62cdb94faa984ac4b8c8
Author: Valentin Olteanu <vo...@adobe.com>
AuthorDate: Wed Feb 21 21:37:28 2018 +0100

    SLING-7509 - Add QueryClient
---
 pom.xml                                            |  50 +++++
 .../sling/testing/clients/query/QueryClient.java   | 218 +++++++++++++++++++++
 .../sling/testing/clients/query/package-info.java  |  25 +++
 .../clients/query/servlet/QueryServlet.java        | 168 ++++++++++++++++
 .../testing/clients/query/QueryClientTest.java     | 167 ++++++++++++++++
 5 files changed, 628 insertions(+)

diff --git a/pom.xml b/pom.xml
index 17c4562..2eac034 100644
--- a/pom.xml
+++ b/pom.xml
@@ -132,6 +132,11 @@
             <artifactId>org.apache.sling.xss</artifactId>
             <version>1.0.4</version>
         </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.tinybundles</groupId>
+            <artifactId>tinybundles</artifactId>
+            <version>3.0.0</version>
+        </dependency>
 
         <!-- For tests -->
         <dependency>
@@ -158,5 +163,50 @@
             <classifier>tests</classifier>
             <scope>test</scope>
         </dependency>
+
+        <!-- Used by QueryServlet on server side -->
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.16.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <version>2.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <!--
+        The dependency below is a duplicate of org.codehaus.jackson,
+        which was renamed to com.fasterxml.jackson.core. Yet, because of
+        package name changes, we cannot automatically switch because it would
+        cause a major change of the API. The new packages are currently needed for
+        the query servlet, but everything should be updated at some point.
+        -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>2.9.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.9.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <version>5.0.0</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/testing/clients/query/QueryClient.java b/src/main/java/org/apache/sling/testing/clients/query/QueryClient.java
new file mode 100644
index 0000000..a6b5d2e
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/query/QueryClient.java
@@ -0,0 +1,218 @@
+/*
+ * 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.
+ */
+package org.apache.sling.testing.clients.query;
+
+import org.apache.http.NameValuePair;
+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.SlingHttpResponse;
+import org.apache.sling.testing.clients.osgi.OsgiConsoleClient;
+import org.apache.sling.testing.clients.query.servlet.QueryServlet;
+import org.apache.sling.testing.clients.util.JsonUtils;
+import org.apache.sling.testing.clients.util.URLParameterBuilder;
+import org.codehaus.jackson.JsonNode;
+import org.ops4j.pax.tinybundles.core.TinyBundles;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+import static org.apache.http.HttpStatus.SC_OK;
+
+/**
+ * <p>Sling client for performing oak queries.</p>
+ *
+ * <p>Uses a custom servlet {@link QueryServlet} to execute the query on the server
+ * and return the results as a json. If the servlet is not yet present, it automatically
+ * installs it and creates the corresponding nodes</p>
+ *
+ * <p>The servlet is exposed under {@value QueryServlet#SERVLET_PATH}.</p>
+ *
+ * <p>The servlet is not automatically uninstalled to avoid too much noise on the instance.
+ * The caller should take care of it, if needed, by calling {@link #uninstallServlet()}</p>
+ */
+public class QueryClient extends SlingClient {
+
+    /**
+     * Query types, as defined in {@code org.apache.jackrabbit.oak.query.QueryEngineImpl}
+     */
+    public enum QueryType {
+        SQL2("JCR-SQL2"),
+        SQL("sql"),
+        XPATH("xpath"),
+        JQOM("JCR-JQOM");
+
+        private final String name;
+
+        QueryType(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public String toString() {
+            return name;
+        }
+    }
+
+    private static final Logger LOG = LoggerFactory.getLogger(QueryClient.class);
+
+    private static final String BUNDLE_BSN = "org.apache.sling.testing.clients.query";
+    private static final String BUNDLE_NAME = "Sling Testing Clients Query Servlet";
+    private static final String BUNDLE_VERSION = "1.0.0";
+
+    private static final long BUNDLE_START_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
+
+    /**
+     * Constructor used by adaptTo
+     *
+     * @param http underlying HttpClient
+     * @param config config state
+     * @throws ClientException if the client cannot be created
+     */
+    public QueryClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+        super(http, config);
+    }
+
+    /**
+     * Convenience constructor
+     *
+     * @param url host url
+     * @param user username
+     * @param password password
+     * @throws ClientException if the client cannot be constructed
+     */
+    public QueryClient(URI url, String user, String password) throws ClientException {
+        super(url, user, password);
+    }
+
+    /**
+     * Executes a query on the server and returns the results as a json
+     *
+     * @param query query to be executed
+     * @param type type of the query
+     * @return the results in json as exported by {@link QueryServlet}
+     * @throws ClientException if the request failed to execute
+     */
+    public JsonNode doQuery(final String query, final QueryType type) throws ClientException, InterruptedException {
+        return doQuery(query, type, true, false);
+    }
+
+    /**
+     * Executes a query on the server and returns only the number of rows in the result
+     *
+     * @param query query to be executed
+     * @param type type of the query
+     * @return total results returned by the query
+     * @throws ClientException if the request failed to execute
+     */
+    public long doCount(final String query, final QueryType type) throws ClientException, InterruptedException {
+        return doQuery(query, type, false, false).get("total").getLongValue();
+    }
+
+    /**
+     * Retrieves the plan of the query. Useful for determining which index is used
+     *
+     * @param query query to be executed
+     * @param type type of the query
+     * @return total results returned by the query
+     * @throws ClientException if the request failed to execute
+     */
+    public String getPlan(final String query, final QueryType type) throws ClientException, InterruptedException {
+        return doQuery(query, type, false, true).get("plan").toString();
+    }
+
+    protected JsonNode doQuery(final String query, final QueryType type, final boolean showResults, final boolean explain)
+            throws ClientException, InterruptedException {
+
+        List<NameValuePair> params = URLParameterBuilder.create()
+                .add("query", query)
+                .add("type", type.toString())
+                .add("showresults", Boolean.toString(showResults))
+                .add("explain", Boolean.toString(explain))
+                .getList();
+
+        try {
+            // try optimistically to execute the query
+            SlingHttpResponse response = this.doGet(QueryServlet.SERVLET_PATH, params, SC_OK);
+            return JsonUtils.getJsonNodeFromString(response.getContent());
+        } catch (ClientException e) {
+            if (e.getHttpStatusCode() == SC_NOT_FOUND) {
+                LOG.info("Could not find query servlet, will try to install it");
+                installServlet();
+                LOG.info("Retrying the query");
+                SlingHttpResponse response = this.doGet(QueryServlet.SERVLET_PATH, params, SC_OK);
+                return JsonUtils.getJsonNodeFromString(response.getContent());
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * <p>Installs the servlet to be able to perform queries.</p>
+     *
+     * <p>By default, methods of this client automatically install the servlet if needed,
+     * so there is no need to explicitly call from outside</p>
+     *
+     * @throws ClientException if the installation fails
+     */
+    public QueryClient installServlet() throws ClientException, InterruptedException {
+        InputStream bundleStream = TinyBundles.bundle()
+                .set("Bundle-SymbolicName", BUNDLE_BSN)
+                .set("Bundle-Version", BUNDLE_VERSION)
+                .set("Bundle-Name", BUNDLE_NAME)
+                .add(QueryServlet.class)
+                .build(TinyBundles.withBnd());
+
+        try {
+            File bundleFile = File.createTempFile(BUNDLE_BSN + "-" + BUNDLE_VERSION, ".jar");
+            Files.copy(bundleStream, bundleFile.toPath(), REPLACE_EXISTING);
+
+            adaptTo(OsgiConsoleClient.class).installBundle(bundleFile, true);
+            adaptTo(OsgiConsoleClient.class).waitBundleStarted(BUNDLE_BSN, BUNDLE_START_TIMEOUT, 100);
+
+            LOG.info("query servlet installed at {}", getUrl(QueryServlet.SERVLET_PATH));
+        } catch (IOException e) {
+            throw new ClientException("Failed to create the query servlet bundle", e);
+        } catch (TimeoutException e) {
+            throw new ClientException("The query servlet bundle did not successfully start", e);
+        }
+
+        return this;
+    }
+
+    /**
+     * Deletes all the resources created by {@link #installServlet()}
+     *
+     * @throws ClientException if any of the resources fails to uninstall
+     */
+    public QueryClient uninstallServlet() throws ClientException {
+        adaptTo(OsgiConsoleClient.class).uninstallBundle(BUNDLE_BSN);
+        return this;
+    }
+}
diff --git a/src/main/java/org/apache/sling/testing/clients/query/package-info.java b/src/main/java/org/apache/sling/testing/clients/query/package-info.java
new file mode 100644
index 0000000..cf0283c
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/query/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+/**
+ * Query tools leveraging javax.jcr.query
+ */
+@Version("0.1.0")
+package org.apache.sling.testing.clients.query;
+
+import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/sling/testing/clients/query/servlet/QueryServlet.java b/src/main/java/org/apache/sling/testing/clients/query/servlet/QueryServlet.java
new file mode 100644
index 0000000..54cdc1f
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/clients/query/servlet/QueryServlet.java
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ */
+package org.apache.sling.testing.clients.query.servlet;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
+import org.osgi.service.component.annotations.Component;
+
+import javax.jcr.Session;
+import javax.jcr.query.*;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.Date;
+
+import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_METHODS;
+import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_PATHS;
+
+@Component(
+        name = QueryServlet.SERVLET_NAME,
+        service = {Servlet.class},
+        property = {
+                SLING_SERVLET_PATHS + "=" + QueryServlet.SERVLET_PATH,
+                SLING_SERVLET_METHODS + "=GET"
+        }
+)
+public class QueryServlet extends SlingSafeMethodsServlet {
+    private static final long serialVersionUID = 1L;
+
+    public static final String SERVLET_PATH = "/system/testing/query";
+    public static final String SERVLET_NAME = "Sling Testing Clients Query Servlet";
+
+    @Override
+    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
+            throws ServletException, IOException {
+
+        response.setContentType("application/json");
+        response.setCharacterEncoding("UTF-8");
+
+        try {
+            final QueryManager qm = request.getResourceResolver().adaptTo(Session.class)
+                    .getWorkspace().getQueryManager();
+
+            long before = 0;
+            long after = 0;
+            long total = 0;
+
+            String query = request.getParameter("query");
+            String type = request.getParameter("type");
+
+            // default for showResults is true, unless parameter is matching exactly "false"
+            boolean showResults = !("false".equalsIgnoreCase(request.getParameter("showresults")));
+            // default for explainQuery is false, unless parameter is present and is not matching "false"
+            String explainParam = request.getParameter("explain");
+            boolean explainQuery = (explainParam != null) && !("false".equalsIgnoreCase(explainParam));
+
+            boolean tidy = false;
+            for (String selector : request.getRequestPathInfo().getSelectors()) {
+                if ("tidy".equals(selector)) {
+                    tidy = true;
+                }
+            }
+
+            if ((query == null) || query.equals("") || (type == null) || type.equals("")) {
+                response.sendError(400, "Parameters query and type are required"); // invalid request
+                return;
+            }
+
+            // prepare
+            if (explainQuery) {
+                query = "explain " + query;
+            }
+
+            Query q = qm.createQuery(query, type);
+
+            // execute
+            before = new Date().getTime();
+            QueryResult result = q.execute();
+            after = new Date().getTime();
+
+            // collect results
+            String firstSelector = null;
+            if (result.getSelectorNames().length > 1) {
+                firstSelector = result.getSelectorNames()[0];
+                try {
+                    String[] columnNames = result.getColumnNames();
+                    if (columnNames.length > 0) {
+                        String firstColumnName = columnNames[0];
+                        int firstDot = firstColumnName.indexOf('.');
+                        if (firstDot > 0) {
+                            firstSelector = firstColumnName.substring(0, firstDot);
+                        }
+                    }
+                } catch (Exception ignored) {
+                }
+            }
+
+            ObjectMapper mapper = new ObjectMapper();
+            ObjectNode responseJson = mapper.createObjectNode();
+
+            if (explainQuery) {
+                responseJson.put("plan", result.getRows().nextRow().getValue("plan").getString());
+            } else if (showResults) {
+                ArrayNode results = mapper.createArrayNode();
+
+                RowIterator rows = result.getRows();
+                while (rows.hasNext()) {
+                    Row row = rows.nextRow();
+                    String rowPath = (firstSelector != null) ? row.getPath(firstSelector) : row.getPath();
+                    String rowType = (firstSelector != null)
+                            ? row.getNode(firstSelector).getPrimaryNodeType().getName()
+                            : row.getNode().getPrimaryNodeType().getName();
+
+                    ObjectNode rowJson = mapper.createObjectNode();
+                    rowJson.put("path", rowPath);
+                    rowJson.put("type", rowType);
+                    results.add(rowJson);
+
+                    total++;
+                }
+
+                responseJson.set("results", results);
+            } else {
+                // only count results
+                RowIterator rows = result.getRows();
+                while (rows.hasNext()) {
+                    rows.nextRow();
+                    total++;
+                }
+            }
+
+            responseJson.put("total", total);
+            responseJson.put("time", after - before);
+
+            if (tidy) {
+                response.getWriter().write(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(responseJson));
+            } else {
+                response.getWriter().write(responseJson.toString());
+            }
+
+        } catch (InvalidQueryException e) {
+            // Consider InvalidQueryException as an invalid request instead of sending 500 server error
+            response.sendError(400, e.getMessage());
+            e.printStackTrace(response.getWriter());
+        } catch (final Exception e) {
+            response.sendError(500, e.getMessage());
+            e.printStackTrace(response.getWriter());
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/testing/clients/query/QueryClientTest.java b/src/test/java/org/apache/sling/testing/clients/query/QueryClientTest.java
new file mode 100644
index 0000000..8e8d6f5
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/clients/query/QueryClientTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.
+ */
+package org.apache.sling.testing.clients.query;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+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.codehaus.jackson.JsonNode;
+import org.junit.Assert;
+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.List;
+
+public class QueryClientTest {
+    private static final Logger LOG = LoggerFactory.getLogger(QueryClientTest.class);
+
+    private static final String QUERY_PATH = "/system/testing/query"; // same as in QueryServlet
+    private static final String BUNDLE_PATH = "/system/console/bundles/org.apache.sling.testing.clients.query";
+    private static final String QUERY_RESPONSE = "{\"total\": 1234,\"time\": 1}";
+    private static final String EXPLAIN_RESPONSE = "{\"plan\": \"some plan\",\"time\": 1}";
+    private static final String JSON_BUNDLE = "{\n" +
+            "  \"status\": \"Bundle information: 546 bundles in total, 537 bundles active, 8 bundles active fragments, 1 bundle resolved.\",\n" +
+            "  \"s\": [\n" +
+            "    546,\n" +
+            "    537,\n" +
+            "    8,\n" +
+            "    1,\n" +
+            "    0\n" +
+            "  ],\n" +
+            "  \"data\": [\n" +
+            "    {\n" +
+            "      \"id\": 560,\n" +
+            "      \"name\": \"Query servlet for testing\",\n" +
+            "      \"fragment\": false,\n" +
+            "      \"stateRaw\": 32,\n" +
+            "      \"state\": \"Active\",\n" +
+            "      \"version\": \"1.0.0\",\n" +
+            "      \"symbolicName\": \"org.apache.sling.testing.clients.query\",\n" +
+            "      \"category\": \"\"\n" +
+            "    }\n" +
+            "  ]\n" +
+            "}";
+
+    @ClassRule
+    public static HttpServerRule httpServer = new HttpServerRule() {
+        @Override
+        protected void registerHandlers() throws IOException {
+
+            // Normal query request
+            serverBootstrap.registerHandler(QUERY_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);
+                }
+            });
+        }
+    };
+
+    private static QueryClient client;
+
+    public QueryClientTest() throws ClientException {
+        client = new QueryClient(httpServer.getURI(), "admin", "admin");
+        // for testing an already running instance
+        // client = new QueryClient(java.net.URI.create("http://localhost:8080"), "admin", "admin");
+    }
+
+    @Test
+    public void testInstallServlet() throws ClientException, InterruptedException {
+        client.installServlet();
+    }
+
+    @Test
+    public void testDoQuery() throws ClientException, InterruptedException {
+        JsonNode response = client.doQuery("SELECT * FROM [nt:file] WHERE ISDESCENDANTNODE([/etc/])",
+//        JsonNode response = client.doQuery("SELECT * FROM [cq:Tag] WHERE ISDESCENDANTNODE([/etc/])",
+                QueryClient.QueryType.SQL2);
+        LOG.info(response.toString());
+        Assert.assertNotEquals(0, response.get("total").getLongValue());
+    }
+
+    @Test
+    public void testDoCount() throws ClientException, InterruptedException {
+        long results = client.doCount("SELECT * FROM [nt:file] WHERE ISDESCENDANTNODE([/etc/])",
+                QueryClient.QueryType.SQL2);
+        LOG.info("results={}", results);
+        Assert.assertNotEquals(0, results);
+    }
+
+    @Test
+    public void testGetPlan() throws ClientException, InterruptedException {
+        String plan = client.getPlan("SELECT * FROM [nt:file] WHERE ISDESCENDANTNODE([/etc/])",
+                QueryClient.QueryType.SQL2);
+        LOG.info("plan={}", plan);
+        Assert.assertNotEquals("", plan);
+    }
+
+    @Test
+    public void testUninstallServlet() throws ClientException {
+        client.uninstallServlet();
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
rombert@apache.org.