lucene-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From gerlowsk...@apache.org
Subject lucene-solr:master: SOLR-12947: Add SolrJ helper for making JSON DSL requests
Date Tue, 06 Nov 2018 12:35:11 GMT
Repository: lucene-solr
Updated Branches:
  refs/heads/master 08fcce4c9 -> 2d95b740d


SOLR-12947: Add SolrJ helper for making JSON DSL requests

The JSON request API is great, but it's hard to use from SolrJ.  This
commit adds 'JsonQueryRequest', which makes it much easier to write
JSON API requests in SolrJ applications.


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/2d95b740
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/2d95b740
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/2d95b740

Branch: refs/heads/master
Commit: 2d95b740db1fa4ae25ccf53432e3060565cc8da2
Parents: 08fcce4
Author: Jason Gerlowski <gerlowskija@apache.org>
Authored: Mon Nov 5 18:36:35 2018 -0500
Committer: Jason Gerlowski <gerlowskija@apache.org>
Committed: Tue Nov 6 07:34:53 2018 -0500

----------------------------------------------------------------------
 solr/solr-ref-guide/src/json-request-api.adoc   |  62 +++-
 .../request/json/DirectJsonQueryRequest.java    |  50 ++++
 .../solrj/request/json/JsonQueryRequest.java    | 290 +++++++++++++++++++
 .../client/solrj/request/json/package-info.java |  21 ++
 .../solr/client/solrj/util/ClientUtils.java     |   3 +-
 .../ref_guide_examples/JsonRequestApiTest.java  | 122 ++++++++
 .../json/JsonQueryRequestIntegrationTest.java   | 286 ++++++++++++++++++
 .../request/json/JsonQueryRequestUnitTest.java  | 220 ++++++++++++++
 8 files changed, 1051 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2d95b740/solr/solr-ref-guide/src/json-request-api.adoc
----------------------------------------------------------------------
diff --git a/solr/solr-ref-guide/src/json-request-api.adoc b/solr/solr-ref-guide/src/json-request-api.adoc
index a46aeb4..bc676cc 100644
--- a/solr/solr-ref-guide/src/json-request-api.adoc
+++ b/solr/solr-ref-guide/src/json-request-api.adoc
@@ -1,4 +1,6 @@
 = JSON Request API
+:solr-root-path: ../../
+:example-source-dir: {solr-root-path}solrj/src/test/org/apache/solr/client/ref_guide_examples/
 :page-children: json-query-dsl
 // Licensed to the Apache Software Foundation (ASF) under one
 // or more contributor license agreements.  See the NOTICE file
@@ -26,12 +28,31 @@ Here's an example of a search request using query parameters only:
 curl "http://localhost:8983/solr/techproducts/query?q=memory&fq=inStock:true"
 
 The same request when passed as JSON in the body:
+[.dynamic-tabs]
+--
+[example.tab-pane#curlsimplejsonquery]
+====
+[.tab-label]*curl*
 [source,bash]
+----
 curl http://localhost:8983/solr/techproducts/query -d '
 {
   "query" : "memory",
   "filter" : "inStock:true"
 }'
+----
+====
+
+[example.tab-pane#solrjsimplejsonquery]
+====
+[.tab-label]*SolrJ*
+
+[source,java,indent=0]
+----
+include::{example-source-dir}JsonRequestApiTest.java[tag=solrj-json-query-simple]
+----
+====
+--
 
 == Passing JSON via Request Parameter
 It may sometimes be more convenient to pass the JSON body as a request parameter rather than in the actual body of the HTTP request. Solr treats a `json` parameter the same as a JSON body.
@@ -104,7 +125,13 @@ Note: `debug=true` as well as `debugQuery=true` might have too much performance
 == Passing Parameters via JSON
 We can also pass normal query request parameters in the JSON body within the params block:
 
+[.dynamic-tabs]
+--
+[example.tab-pane#curljsonqueryparamsblock]
+====
+[.tab-label]*curl*
 [source,bash]
+----
 curl "http://localhost:8983/solr/techproducts/query?fl=name,price"-d '
 {
   params: {
@@ -112,6 +139,19 @@ curl "http://localhost:8983/solr/techproducts/query?fl=name,price"-d '
     rows: 1
   }
 }'
+----
+====
+
+[example.tab-pane#solrjjsonqueryparamsblock]
+====
+[.tab-label]*SolrJ*
+
+[source,java,indent=0]
+----
+include::{example-source-dir}JsonRequestApiTest.java[tag=solrj-json-query-params-block]
+----
+====
+--
 
 Which is equivalent to
 
@@ -167,9 +207,27 @@ And we get an error back containing the error string:
 Of course request templating via parameter substitution works fully with JSON request bodies or parameters as well.
 For example:
 
+[.dynamic-tabs]
+--
+[example.tab-pane#curljsonquerymacroexpansion]
+====
+[.tab-label]*curl*
 [source,bash]
-curl "http://localhost:8983/solr/techproducts/query?FIELD=text&TERM=memory&HOWMANY=10" -d '
+----
+curl "http://localhost:8983/solr/techproducts/query?FIELD=text&TERM=memory" -d '
 {
   query:"${FIELD}:${TERM}",
-  limit:${HOWMANY}
 }'
+----
+====
+
+[example.tab-pane#solrjjsonquerymacroexpansion]
+====
+[.tab-label]*SolrJ*
+
+[source,java,indent=0]
+----
+include::{example-source-dir}JsonRequestApiTest.java[tag=solrj-json-query-macro-expansion]
+----
+====
+--

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2d95b740/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequest.java
new file mode 100644
index 0000000..43e56dc
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/DirectJsonQueryRequest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.solr.client.solrj.request.json;
+
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.request.RequestWriter;
+import org.apache.solr.client.solrj.util.ClientUtils;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+
+/**
+ * Represents a query using the <a href="https://lucene.apache.org/solr/guide/json-request-api.html">JSON Query DSL</a>
+ *
+ * This class doesn't construct the request body itself.  It uses a provided String without any modification.  Often
+ * used in combination with the JSON DSL's <a href="https://lucene.apache.org/solr/guide/json-request-api.html#parameter-substitution-macro-expansion">macro expansion capabilities</a>.
+ * The JSON body can contain template parameters which are replaced with values fetched from the {@link SolrParams}
+ * used by this request.  For a more flexible, guided approach to constructing JSON DSL requests, see
+ * {@link JsonQueryRequest}.
+ */
+public class DirectJsonQueryRequest extends QueryRequest {
+  private final String jsonString;
+
+  public DirectJsonQueryRequest(String jsonString) {
+    this(jsonString, new ModifiableSolrParams());
+  }
+
+  public DirectJsonQueryRequest(String jsonString, SolrParams params) {
+    super(params, METHOD.POST);
+    this.jsonString = jsonString;
+  }
+
+  public RequestWriter.ContentWriter getContentWriter(String expectedType) {
+    return new RequestWriter.StringPayloadContentWriter(jsonString, ClientUtils.TEXT_JSON);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2d95b740/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java
new file mode 100644
index 0000000..3a570d0
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/JsonQueryRequest.java
@@ -0,0 +1,290 @@
+/*
+ * 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.solr.client.solrj.request.json;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.client.solrj.request.QueryRequest;
+import org.apache.solr.client.solrj.request.RequestWriter;
+import org.apache.solr.client.solrj.util.ClientUtils;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.Utils;
+
+/**
+ * Represents a query using the <a href="https://lucene.apache.org/solr/guide/json-request-api.html">JSON Query DSL</a>
+ *
+ * This class constructs the request using setters for individual properties.  For a more monolithic approach to
+ * constructing the JSON request, see {@link DirectJsonQueryRequest}
+ */
+public class JsonQueryRequest extends QueryRequest {
+  private final Map<String, Object> jsonRequestMap;
+
+  /**
+   * Creates a {@link JsonQueryRequest} with an empty {@link SolrParams} object
+   */
+  public JsonQueryRequest() {
+    this(new ModifiableSolrParams());
+  }
+
+  /**
+   * Creates a {@link JsonQueryRequest} using the provided {@link SolrParams}
+   */
+  public JsonQueryRequest(SolrParams params) {
+    super(params, METHOD.POST);
+    this.jsonRequestMap = new HashMap<>();
+  }
+
+  /**
+   * Specify the query sent as a part of this JSON request
+   *
+   * This method may be called multiple times, but each call overwrites the value specified by previous calls.
+   *
+   * @param query a String in either of two formats: a query string for the default deftype (e.g. "title:solr"), or a
+   *              localparams query (e.g. "{!lucene df=text v='solr'}" )
+   *
+   * @throws IllegalArgumentException if {@code query} is null
+   */
+  public JsonQueryRequest setQuery(String query) {
+    if (query == null) {
+      throw new IllegalArgumentException("'query' parameter must be non-null");
+    }
+    jsonRequestMap.put("query", query);
+    return this;
+  }
+
+  /**
+   * Specify the query sent as a part of this JSON request.
+   *
+   * This method may be called multiple times, but each call overwrites the value specified by previous calls.
+   * <p>
+   * <b>Example:</b> You wish to send the JSON request: "{'limit': 5, 'query': {'lucene': {'df':'genre_s', 'query': 'scifi'}}}".  The
+   * query subtree of this request is: "{'lucene': {'df': 'genre_s', 'query': 'scifi'}}".  You would represent this query
+   * JSON as follows:
+   * <pre>{@code
+   *     final Map<String, Object> queryMap = new HashMap<>();
+   *     final Map<String, Object> luceneQueryParamMap = new HashMap<>();
+   *     queryMap.put("lucene", luceneQueryParamMap);
+   *     luceneQueryParamMap.put("df", "genre_s");
+   *     luceneQueryParamMap.put("query", "scifi");
+   * }</pre>
+   *
+   * @param queryJson a Map of values representing the query subtree of the JSON request you wish to send.
+   * @throws IllegalArgumentException if {@code queryJson} is null.
+   */
+  public JsonQueryRequest setQuery(Map<String, Object> queryJson) {
+    if (queryJson == null) {
+      throw new IllegalArgumentException("'queryJson' parameter must be non-null");
+    }
+    jsonRequestMap.put("query", queryJson);
+    return this;
+  }
+
+  /**
+   * Specify whether results should be fetched starting from a particular offset (or 'start').
+   *
+   * Defaults to 0 if not set.
+   *
+   * @param offset a non-negative integer representing the offset (or 'start') to use when returning results
+   *
+   * @throws IllegalArgumentException if {@code offset} is negative
+   */
+  public JsonQueryRequest setOffset(int offset) {
+    if (offset < 0) {
+      throw new IllegalArgumentException("'offset' parameter must be non-negative");
+    }
+    jsonRequestMap.put("offset", offset);
+    return this;
+  }
+
+  /**
+   * Specify how many results should be returned from the JSON request
+   *
+   * @param limit a non-negative integer representing the maximum results to return from a search
+   * @throws IllegalArgumentException if {@code limit} is negative
+   */
+  public JsonQueryRequest setLimit(int limit) {
+    if (limit < 0) {
+      throw new IllegalArgumentException("'limit' parameter must be non-negative");
+    }
+    jsonRequestMap.put("limit", limit);
+    return this;
+  }
+
+  /**
+   * Specify how results to the JSON request should be sorted before being returned by Solr
+   *
+   * @param sort a string representing the desired result sort order (e.g. "price asc")
+   *
+   * @throws IllegalArgumentException if {@code sort} is null
+   */
+  public JsonQueryRequest setSort(String sort) {
+    if (sort == null) {
+      throw new IllegalArgumentException("'sort' parameter must be non-null");
+    }
+    jsonRequestMap.put("sort", sort);
+    return this;
+  }
+
+  /**
+   * Add a filter query to run as a part of the JSON request
+   *
+   * This method may be called multiple times; each call will add a new filter to the request
+   *
+   * @param filterQuery a String in either of two formats: a query string for the default deftype (e.g. "title:solr"), or a
+   *                    localparams query (e.g. "{!lucene df=text v='solr'}" )
+   * @throws IllegalArgumentException if {@code filterQuery} is null
+   */
+  public JsonQueryRequest withFilter(String filterQuery) {
+    if (filterQuery == null) {
+      throw new IllegalArgumentException("'filterQuery' must be non-null");
+    }
+    jsonRequestMap.putIfAbsent("filter", new ArrayList<Object>());
+    final List<Object> filters = (List<Object>) jsonRequestMap.get("filter");
+    filters.add(filterQuery);
+    return this;
+  }
+
+  /**
+   * Add a filter query to run as a part of the JSON request
+   *
+   * This method may be called multiple times; each call will add a new filter to the request
+   * <p>
+   * <b>Example:</b> You wish to send the JSON request: "{'query':'*:*', 'filter': [{'lucene': {'df':'genre_s', 'query': 'scifi'}}]}".
+   * The filter you want to add is: "{'lucene': {'df': 'genre_s', 'query': 'scifi'}}".  You would represent this filter
+   * query as follows:
+   * <pre>{@code
+   *     final Map<String, Object> filterMap = new HashMap<>();
+   *     final Map<String, Object> luceneQueryParamMap = new HashMap<>();
+   *     filterMap.put("lucene", luceneQueryParamMap);
+   *     luceneQueryParamMap.put("df", "genre_s");
+   *     luceneQueryParamMap.put("query", "scifi");
+   * }</pre>
+   *
+   * @param filterQuery a Map of values representing the filter request you wish to send.
+   * @throws IllegalArgumentException if {@code filterQuery} is null
+   */
+  public JsonQueryRequest withFilter(Map<String, Object> filterQuery) {
+    if (filterQuery == null) {
+      throw new IllegalArgumentException("'filterQuery' parameter must be non-null");
+    }
+    jsonRequestMap.putIfAbsent("filter", new ArrayList<Object>());
+    final List<Object> filters = (List<Object>) jsonRequestMap.get("filter");
+    filters.add(filterQuery);
+    return this;
+  }
+
+  /**
+   * Specify fields which should be returned by the JSON request.
+   *
+   * This method may be called multiple times; each call will add a new field to the list of those to be returned.
+   *
+   * @param fieldNames the field names that should be returned by the request
+   */
+  public JsonQueryRequest returnFields(String... fieldNames) {
+    jsonRequestMap.putIfAbsent("fields", new ArrayList<String>());
+    final List<String> fields = (List<String>) jsonRequestMap.get("fields");
+    for (String fieldName : fieldNames) {
+      fields.add(fieldName);
+    }
+    return this;
+  }
+
+  /**
+   * Specify fields which should be returned by the JSON request.
+   *
+   * This method may be called multiple times; each call will add a new field to the list of those to be returned.
+   *
+   * @param fieldNames the field names that should be returned by the request
+   * @throws IllegalArgumentException if {@code fieldNames} is null
+   */
+  public JsonQueryRequest returnFields(Iterable<String> fieldNames) {
+    if (fieldNames == null) {
+      throw new IllegalArgumentException("'fieldNames' parameter must be non-null");
+    }
+    jsonRequestMap.putIfAbsent("fields", new ArrayList<String>());
+    final List<String> fields = (List<String>) jsonRequestMap.get("fields");
+    for (String fieldName : fieldNames) {
+      fields.add(fieldName);
+    }
+    return this;
+  }
+
+  /**
+   * Add a property to the "params" block supported by the JSON query DSL
+   *
+   * The JSON query DSL has special support for a few query parameters (limit/rows, offset/start, filter/fq, etc.).  But
+   * many other query parameters are not explicitly covered by the query DSL.  This method can be used to add any of
+   * these other parameters to the JSON request.
+   * <p>
+   * This method may be called multiple times; each call with a different {@code name} will add a new param name/value
+   * to the params subtree. Invocations that repeat a {@code name} will overwrite the previously specified parameter
+   * values associated with that name.
+   *
+   * @param name the name of the parameter to add
+   * @param value the value of the parameter to add.  Usually a String, Number (Integer, Long, Double), or Boolean.
+   *
+   * @throws IllegalArgumentException if either {@code name} or {@code value} are null
+   */
+  public JsonQueryRequest withParam(String name, Object value) {
+    if (name == null) {
+      throw new IllegalArgumentException("'name' parameter must be non-null");
+    }
+    if (value == null) {
+      throw new IllegalArgumentException("'value' parameter must be non-null");
+    }
+
+    jsonRequestMap.putIfAbsent("params", new HashMap<String, Object>());
+    final Map<String, Object> miscParamsMap = (Map<String, Object>) jsonRequestMap.get("params");
+    miscParamsMap.put(name, value);
+    return this;
+  }
+
+  public RequestWriter.ContentWriter getContentWriter(String expectedType) {
+    return new RequestWriter.ContentWriter() {
+      @Override
+      public void write(OutputStream os) throws IOException {
+        //TODO consider whether using Utils.writeJson would work here as that'd be more mem efficient
+        OutputStreamWriter writer = new OutputStreamWriter(os, StandardCharsets.UTF_8);
+
+        writer.write(Utils.toJSONString(jsonRequestMap));
+        writer.flush();
+      }
+
+      @Override
+      public String getContentType() {
+        return ClientUtils.TEXT_JSON;
+      }
+    };
+  }
+
+  @Override
+  public void setMethod(METHOD m) {
+    if (METHOD.POST != m) {
+      final String message = getClass().getName() + " only supports POST for sending JSON queries.";
+      throw new UnsupportedOperationException(message);
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2d95b740/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/package-info.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/package-info.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/package-info.java
new file mode 100644
index 0000000..f9e9533
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/json/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+ 
+/** 
+ * Allows sending of requests using Solr's JSON query/faceting API
+ */
+package org.apache.solr.client.solrj.request.json;

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2d95b740/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java b/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
index 26a188d..2a1dfad 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java
@@ -42,7 +42,8 @@ import org.apache.solr.common.util.XML;
 public class ClientUtils 
 {
   // Standard Content types
-  public static final String TEXT_XML = "application/xml; charset=UTF-8";  
+  public static final String TEXT_XML = "application/xml; charset=UTF-8";
+  public static final String TEXT_JSON = "application/json; charset=UTF-8";
   
   /**
    * Take a string and make it an iterable ContentStream

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2d95b740/solr/solrj/src/test/org/apache/solr/client/ref_guide_examples/JsonRequestApiTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/ref_guide_examples/JsonRequestApiTest.java b/solr/solrj/src/test/org/apache/solr/client/ref_guide_examples/JsonRequestApiTest.java
new file mode 100644
index 0000000..b941f2d
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/ref_guide_examples/JsonRequestApiTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.solr.client.ref_guide_examples;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.json.JsonQueryRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.UpdateResponse;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.util.ExternalPaths;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Example SolrJ usage of the JSON Request API.
+ *
+ * Snippets surrounded by "tag" and "end" comments are extracted and used in the Solr Reference Guide.
+ */
+public class JsonRequestApiTest extends SolrCloudTestCase {
+  private static final String COLLECTION_NAME = "techproducts";
+  private static final String CONFIG_NAME = "techproducts_config";
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(1)
+        .addConfig(CONFIG_NAME, new File(ExternalPaths.TECHPRODUCTS_CONFIGSET).toPath())
+        .configure();
+
+    final List<String> solrUrls = new ArrayList<>();
+    solrUrls.add(cluster.getJettySolrRunner(0).getBaseUrl().toString());
+
+    CollectionAdminRequest.createCollection(COLLECTION_NAME, CONFIG_NAME, 1, 1).process(cluster.getSolrClient());
+
+    ContentStreamUpdateRequest up = new ContentStreamUpdateRequest("/update");
+    up.setParam("collection", COLLECTION_NAME);
+    up.addFile(getFile("solrj/docs2.xml"), "application/xml"); // A subset of the 'techproducts' documents
+    up.setAction(AbstractUpdateRequest.ACTION.COMMIT, true, true);
+    UpdateResponse updateResponse = up.process(cluster.getSolrClient());
+    assertEquals(0, updateResponse.getStatus());
+  }
+
+  @Test
+  public void testSimpleJsonQuery() throws Exception {
+    SolrClient solrClient = cluster.getSolrClient();
+    final int expectedResults = 3;
+
+    // tag::solrj-json-query-simple[]
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("memory")
+        .withFilter("inStock:true");
+    QueryResponse queryResponse = simpleQuery.process(solrClient, COLLECTION_NAME);
+    // end::solrj-json-query-simple[]
+
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(expectedResults, queryResponse.getResults().size());
+  }
+
+  @Test
+  public void testJsonQueryUsingParamsBlock() throws Exception {
+    SolrClient solrClient = cluster.getSolrClient();
+
+    //tag::solrj-json-query-params-block[]
+    final ModifiableSolrParams params = new ModifiableSolrParams();
+    params.set("fl", "name", "price");
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest(params)
+        .withParam("q", "memory")
+        .withParam("rows", 1);
+    QueryResponse queryResponse = simpleQuery.process(solrClient, COLLECTION_NAME);
+    // end::solrj-json-query-params-block[]
+
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(1, queryResponse.getResults().size());
+    final SolrDocument doc = queryResponse.getResults().get(0);
+    final Collection<String> returnedFields = doc.getFieldNames();
+    assertEquals(2, doc.getFieldNames().size());
+    assertTrue("Expected returned field list to include 'name'", returnedFields.contains("name"));
+    assertTrue("Expected returned field list to include 'price'", returnedFields.contains("price"));
+  }
+
+  @Test
+  public void testJsonQueryMacroExpansion() throws Exception {
+    SolrClient solrClient = cluster.getSolrClient();
+
+    //tag::solrj-json-query-macro-expansion[]
+    final ModifiableSolrParams params = new ModifiableSolrParams();
+    params.set("FIELD", "text");
+    params.set("TERM", "memory");
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest(params)
+        .setQuery("${FIELD}:${TERM}");
+    QueryResponse queryResponse = simpleQuery.process(solrClient, COLLECTION_NAME);
+    // end::solrj-json-query-macro-expansion[]
+
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(3, queryResponse.getResults().size());
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2d95b740/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestIntegrationTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestIntegrationTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestIntegrationTest.java
new file mode 100644
index 0000000..807f8b6
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestIntegrationTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.solr.client.solrj.request.json;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.client.solrj.request.AbstractUpdateRequest;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.ContentStreamUpdateRequest;
+import org.apache.solr.client.solrj.request.json.JsonQueryRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.UpdateResponse;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.params.ModifiableSolrParams;
+import org.apache.solr.util.ExternalPaths;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Integration tests for {@link JsonQueryRequest}
+ */
+public class JsonQueryRequestIntegrationTest extends SolrCloudTestCase {
+
+  private static final String COLLECTION_NAME = "books";
+  private static final String CONFIG_NAME = "techproducts_config";
+
+  private static final int NUM_BOOKS_TOTAL = 10;
+  private static final int NUM_SCIFI_BOOKS = 2;
+  private static final int NUM_IN_STOCK = 8;
+  private static final int NUM_IN_STOCK_AND_FIRST_IN_SERIES = 5;
+
+  @BeforeClass
+  public static void setupCluster() throws Exception {
+    configureCluster(1)
+        .addConfig(CONFIG_NAME, new File(ExternalPaths.TECHPRODUCTS_CONFIGSET).toPath())
+        .configure();
+
+    final List<String> solrUrls = new ArrayList<>();
+    solrUrls.add(cluster.getJettySolrRunner(0).getBaseUrl().toString());
+
+    CollectionAdminRequest.createCollection(COLLECTION_NAME, CONFIG_NAME, 1, 1).process(cluster.getSolrClient());
+
+    ContentStreamUpdateRequest up = new ContentStreamUpdateRequest("/update");
+    up.setParam("collection", COLLECTION_NAME);
+    up.addFile(getFile("solrj/books.csv"), "application/csv");
+    up.setAction(AbstractUpdateRequest.ACTION.COMMIT, true, true);
+    UpdateResponse updateResponse = up.process(cluster.getSolrClient());
+    assertEquals(0, updateResponse.getStatus());
+  }
+
+  @Test
+  public void testEmptyJson() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest();
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    // No q.alt in techproducts configset, so request should gracefully find no results
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(0, queryResponse.getResults().getNumFound());
+  }
+
+  @Test
+  public void testCanRunSimpleQueries() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("*:*");
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_BOOKS_TOTAL, queryResponse.getResults().getNumFound());
+  }
+
+  @Test
+  public void testQueriesCanUseLocalParamsSyntax() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("{!lucene df=genre_s v='scifi'}");
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_SCIFI_BOOKS, queryResponse.getResults().getNumFound());
+  }
+
+
+  @Test
+  public void testQueriesCanUseExpandedSyntax() throws Exception {
+    //Construct a tree representing the JSON: {lucene: {df:'genre_s', 'query': 'scifi'}}
+    final Map<String, Object> queryMap = new HashMap<>();
+    final Map<String, Object> luceneQueryParamMap = new HashMap<>();
+    queryMap.put("lucene", luceneQueryParamMap);
+    luceneQueryParamMap.put("df", "genre_s");
+    luceneQueryParamMap.put("query", "scifi");
+
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery(queryMap);
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_SCIFI_BOOKS, queryResponse.getResults().getNumFound());
+  }
+
+  @Test
+  public void testQueriesCanBeNested() throws Exception {
+    final Map<String, Object> queryJsonMap = new HashMap<>();
+    final Map<String, Object> clausesJsonMap = new HashMap<>();
+    queryJsonMap.put("bool", clausesJsonMap);
+    clausesJsonMap.put("must", "genre_s:scifi");
+    clausesJsonMap.put("must_not", "series_t:Ender");
+
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery(queryJsonMap);
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(1, queryResponse.getResults().getNumFound()); // 2 scifi books, only 1 is NOT "Ender's Game"
+  }
+
+  @Test
+  public void testFiltersCanBeAddedToQueries() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFilter("inStock:true");
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_IN_STOCK, queryResponse.getResults().getNumFound());
+  }
+
+  @Test
+  public void testFiltersCanUseLocalParamsSyntax() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFilter("{!lucene df=inStock v='true'}");
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_IN_STOCK, queryResponse.getResults().getNumFound());
+  }
+
+  @Test
+  public void testFiltersCanUseExpandedSyntax() throws Exception {
+    final Map<String, Object> filterJsonMap = new HashMap<>();
+    final Map<String, Object> luceneQueryParamsMap = new HashMap<>();
+    filterJsonMap.put("lucene", luceneQueryParamsMap);
+    luceneQueryParamsMap.put("df", "genre_s");
+    luceneQueryParamsMap.put("query", "scifi");
+
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFilter(filterJsonMap);
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_SCIFI_BOOKS, queryResponse.getResults().getNumFound());
+  }
+
+  @Test
+  public void testMultipleFiltersCanBeUsed() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("*:*")
+        .withFilter("sequence_i:1") // 7 books are the first of a series
+        .withFilter("inStock:true");// but only 5 are in stock
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_IN_STOCK_AND_FIRST_IN_SERIES, queryResponse.getResults().getNumFound());
+  }
+
+  @Test
+  public void canSpecifyFieldsToBeReturned() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("*:*")
+        .returnFields("id", "name");
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    final SolrDocumentList docs = queryResponse.getResults();
+    assertEquals(NUM_BOOKS_TOTAL, docs.getNumFound());
+    for (SolrDocument returnedDoc : docs) {
+      final Collection<String> fields = returnedDoc.getFieldNames();
+      assertEquals(2, fields.size());
+      assertTrue("Expected field list to contain 'id'", fields.contains("id"));
+      assertTrue("Expected field list to contain 'name'", fields.contains("name"));
+    }
+  }
+
+  @Test
+  public void testObeysResultLimit() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("*:*")
+        .setLimit(5);
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_BOOKS_TOTAL, queryResponse.getResults().getNumFound());
+    assertEquals(5, queryResponse.getResults().size());
+  }
+
+  @Test
+  public void testAcceptsTraditionalQueryParamNamesInParamsBlock() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .withParam("q", "*:*")
+        .withParam("rows", 4);
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_BOOKS_TOTAL, queryResponse.getResults().getNumFound());
+    assertEquals(4, queryResponse.getResults().size());
+  }
+
+  @Test
+  public void testReturnsResultsStartingAtOffset() throws Exception {
+    final JsonQueryRequest originalDocsQuery = new JsonQueryRequest()
+        .setQuery("*:*");
+    QueryResponse originalDocsResponse = originalDocsQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, originalDocsResponse.getStatus());
+    assertEquals(NUM_BOOKS_TOTAL, originalDocsResponse.getResults().size());
+    final SolrDocumentList originalDocs = originalDocsResponse.getResults();
+
+    final int offset = 2;
+    final JsonQueryRequest offsetDocsQuery = new JsonQueryRequest()
+        .setQuery("*:*")
+        .setOffset(offset);
+    QueryResponse offsetDocsResponse = offsetDocsQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, offsetDocsResponse.getStatus());
+    assertEquals(NUM_BOOKS_TOTAL - offset, offsetDocsResponse.getResults().size());
+    final SolrDocumentList offsetDocs = offsetDocsResponse.getResults();
+
+    // Ensure the same docs are returned, shifted by 'offset'
+    for (int i = 0; i < offsetDocs.size(); i++) {
+      final String offsetId = (String) offsetDocs.get(i).getFieldValue("id");
+      final String originalId = (String) originalDocs.get(i + offset).getFieldValue("id");
+      assertEquals(offsetId, originalId);
+    }
+  }
+
+  @Test
+  public void testReturnsReturnsResultsWithSpecifiedSort() throws Exception {
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest()
+        .setQuery("*:*")
+        .setSort("price desc");
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_BOOKS_TOTAL, queryResponse.getResults().getNumFound());
+    final SolrDocumentList docs = queryResponse.getResults();
+    for (int i = 0; i < docs.size() - 1; i++) {
+      final float pricierDocPrice = (Float) docs.get(i).getFieldValue("price");
+      final float cheaperDocPrice = (Float) docs.get(i+1).getFieldValue("price");
+      assertTrue("Expected doc at index " + i + " doc to be more expensive than doc at " + (i+1),
+          pricierDocPrice >= cheaperDocPrice);
+    }
+  }
+
+  @Test
+  public void testCombinesJsonParamsWithUriParams() throws Exception {
+    final ModifiableSolrParams params = new ModifiableSolrParams();
+    params.set("fq", "inStock:true");
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest(params)
+        .setQuery("*:*");
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_IN_STOCK, queryResponse.getResults().getNumFound());
+  }
+
+  @Test
+  public void testExpandsParameterMacros() throws Exception {
+    final ModifiableSolrParams params = new ModifiableSolrParams();
+    params.set("FIELD", "inStock");
+    params.set("VALUE", "true");
+    final JsonQueryRequest simpleQuery = new JsonQueryRequest(params)
+        .setQuery("${FIELD}:${VALUE}");
+    QueryResponse queryResponse = simpleQuery.process(cluster.getSolrClient(), COLLECTION_NAME);
+    assertEquals(0, queryResponse.getStatus());
+    assertEquals(NUM_IN_STOCK, queryResponse.getResults().getNumFound());
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2d95b740/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java
new file mode 100644
index 0000000..c6661fe
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/client/solrj/request/json/JsonQueryRequestUnitTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.solr.client.solrj.request.json;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.solr.client.solrj.request.RequestWriter;
+import org.apache.solr.client.solrj.request.json.JsonQueryRequest;
+import org.apache.solr.client.solrj.util.ClientUtils;
+import org.junit.Test;
+import static org.junit.internal.matchers.StringContains.containsString;
+
+/**
+ * Unit tests for {@link JsonQueryRequest}
+ */
+public class JsonQueryRequestUnitTest extends LuceneTestCase {
+
+  private static final boolean LEAVE_WHITESPACE = false;
+
+  @Test
+  public void testRejectsNullQueryString() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().setQuery((String)null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testRejectsNullQueryMap() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().setQuery((Map<String, Object>)null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testWritesProvidedQueryStringToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().setQuery("text:solr");
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"query\":\"text:solr\""));
+  }
+
+  @Test
+  public void testWritesProvidedQueryMapToJsonCorrectly() {
+    final Map<String, Object> queryMap = new HashMap<>();
+    final Map<String, Object> paramsMap = new HashMap<>();
+    queryMap.put("lucene", paramsMap);
+    paramsMap.put("df", "text");
+    paramsMap.put("q", "*:*");
+    final JsonQueryRequest request = new JsonQueryRequest().setQuery(queryMap);
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"query\":{\"lucene\":{\"q\":\"*:*\",\"df\":\"text\"}}"));
+  }
+
+  @Test
+  public void testRejectsInvalidLimit() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().setLimit(-1);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-negative"));
+  }
+
+  @Test
+  public void testWritesProvidedLimitToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().setLimit(5);
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"limit\":5"));
+  }
+
+  @Test
+  public void testRejectsInvalidOffset() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().setOffset(-1);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-negative"));
+
+  }
+
+  @Test
+  public void testWritesProvidedOffsetToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().setOffset(5);
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"offset\":5"));
+  }
+
+  @Test
+  public void testRejectsInvalidSort() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().setSort(null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+
+  }
+
+  @Test
+  public void testWritesProvidedSortToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().setSort("price asc");
+    final String requestBody = writeRequestToJson(request, LEAVE_WHITESPACE);
+    assertThat(requestBody, containsString("\"sort\":\"price asc"));
+  }
+
+  @Test
+  public void testRejectsInvalidFilterString() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().withFilter((String)null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testRejectsInvalidFilterMap() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().withFilter((Map<String,Object>)null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testWritesProvidedFilterToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().withFilter("text:solr");
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"filter\":[\"text:solr\"]"));
+  }
+
+  @Test
+  public void testWritesMultipleProvidedFiltersToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().withFilter("text:solr").withFilter("text:lucene");
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"filter\":[\"text:solr\",\"text:lucene\"]"));
+  }
+
+  @Test
+  public void testRejectsInvalidFieldsIterable() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().returnFields((Iterable<String>)null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testWritesProvidedFieldsToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().returnFields("price");
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"fields\":[\"price\"]"));
+  }
+
+  @Test
+  public void testWritesMultipleProvidedFieldsToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().returnFields("price", "name");
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"fields\":[\"price\",\"name\"]"));
+  }
+
+  @Test
+  public void testRejectsInvalidMiscParamName() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().withParam(null, "any-value");
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+  }
+
+  @Test
+  public void testRejectsInvalidMiscParamValue() {
+    Throwable thrown = expectThrows(IllegalArgumentException.class, () -> {
+      new JsonQueryRequest().withParam("any-name", null);
+    });
+    assertThat(thrown.getMessage(),containsString("must be non-null"));
+
+  }
+
+  @Test
+  public void testWritesMiscParamsToJsonCorrectly() {
+    final JsonQueryRequest request = new JsonQueryRequest().withParam("fq", "inStock:true");
+    final String requestBody = writeRequestToJson(request);
+    assertThat(requestBody, containsString("\"params\":{\"fq\":\"inStock:true\"}"));
+  }
+
+  private String writeRequestToJson(JsonQueryRequest request, boolean trimWhitespace) {
+    final RequestWriter.ContentWriter writer = request.getContentWriter(ClientUtils.TEXT_JSON);
+    final ByteArrayOutputStream os = new ByteArrayOutputStream();
+    try {
+      writer.write(os);
+      final String rawJsonString = new String(os.toByteArray(), StandardCharsets.UTF_8);
+      // Trimming whitespace makes our assertions in these tests more stable (independent of JSON formatting) so we do
+      // it by default.  But we leave the option open in case the JSON fields have spaces.
+      if (trimWhitespace) {
+        return rawJsonString.replaceAll("\n", "").replaceAll(" ","");
+      } else {
+        return rawJsonString;
+      }
+    } catch (IOException e) {
+      /* Unreachable in practice, since we're not doing any I/O here */
+      throw new RuntimeException(e);
+    }
+  }
+
+  private String writeRequestToJson(JsonQueryRequest request) {
+    return writeRequestToJson(request, true);
+  }
+}


Mime
View raw message