blob: 6d45634da78bef4af0848a3bb2eee28213d1645e [file] [log] [blame]
/*
* 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.response;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import org.apache.solr.JSONTestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.JsonTextWriter;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.ReturnFields;
import org.apache.solr.search.SolrReturnFields;
import org.junit.BeforeClass;
import org.junit.Test;
/** Test some aspects of JSON/python writer output (very incomplete) */
public class JSONWriterTest extends SolrTestCaseJ4 {
@BeforeClass
public static void beforeClass() throws Exception {
initCore("solrconfig.xml", "schema.xml");
}
private void jsonEq(String expected, String received) {
expected = expected.trim();
received = received.trim();
assertEquals(expected, received);
}
@Test
public void testTypes() throws IOException {
SolrQueryRequest req = req("q", "dummy", "indent", "off");
SolrQueryResponse rsp = new SolrQueryResponse();
QueryResponseWriter w = new PythonResponseWriter();
StringWriter buf = new StringWriter();
rsp.add("data1", Float.NaN);
rsp.add("data2", Double.NEGATIVE_INFINITY);
rsp.add("data3", Float.POSITIVE_INFINITY);
w.write(buf, req, rsp);
jsonEq(buf.toString(), "{'data1':float('NaN'),'data2':-float('Inf'),'data3':float('Inf')}");
w = new RubyResponseWriter();
buf = new StringWriter();
w.write(buf, req, rsp);
jsonEq(buf.toString(), "{'data1'=>(0.0/0.0),'data2'=>-(1.0/0.0),'data3'=>(1.0/0.0)}");
w = new JSONResponseWriter();
buf = new StringWriter();
w.write(buf, req, rsp);
jsonEq(buf.toString(), "{\"data1\":\"NaN\",\"data2\":\"-Infinity\",\"data3\":\"Infinity\"}");
req.close();
}
@Test
public void testJSON() throws IOException {
final String[] namedListStyles =
new String[] {
JsonTextWriter.JSON_NL_FLAT,
JsonTextWriter.JSON_NL_MAP,
JsonTextWriter.JSON_NL_ARROFARR,
JsonTextWriter.JSON_NL_ARROFMAP,
JsonTextWriter.JSON_NL_ARROFNTV,
};
for (final String namedListStyle : namedListStyles) {
implTestJSON(namedListStyle);
}
assertEquals(JSONWriter.JSON_NL_STYLE_COUNT, namedListStyles.length);
}
private void implTestJSON(final String namedListStyle) throws IOException {
SolrQueryRequest req = req("wt", "json", "json.nl", namedListStyle, "indent", "off");
SolrQueryResponse rsp = new SolrQueryResponse();
JSONResponseWriter w = new JSONResponseWriter();
StringWriter buf = new StringWriter();
NamedList<Object> nl = new NamedList<>();
// make sure that 2028 and 2029 are both escaped (they are illegal in javascript)
nl.add("data1", "he\u2028llo\u2029!");
nl.add(null, 42);
nl.add(null, null);
rsp.add("nl", nl);
rsp.add("byte", (byte) -3);
rsp.add("short", (short) -4);
rsp.add("bytes", "abc".getBytes(StandardCharsets.UTF_8));
w.write(buf, req, rsp);
final String expectedNLjson;
if (Objects.equals(namedListStyle, JSONWriter.JSON_NL_FLAT)) {
expectedNLjson = "\"nl\":[\"data1\",\"he\\u2028llo\\u2029!\",null,42,null,null]";
} else if (Objects.equals(namedListStyle, JSONWriter.JSON_NL_MAP)) {
expectedNLjson = "\"nl\":{\"data1\":\"he\\u2028llo\\u2029!\",\"\":42,\"\":null}";
} else if (Objects.equals(namedListStyle, JSONWriter.JSON_NL_ARROFARR)) {
expectedNLjson = "\"nl\":[[\"data1\",\"he\\u2028llo\\u2029!\"],[null,42],[null,null]]";
} else if (Objects.equals(namedListStyle, JSONWriter.JSON_NL_ARROFMAP)) {
expectedNLjson = "\"nl\":[{\"data1\":\"he\\u2028llo\\u2029!\"},42,null]";
} else if (Objects.equals(namedListStyle, JSONWriter.JSON_NL_ARROFNTV)) {
expectedNLjson =
"\"nl\":[{\"name\":\"data1\",\"type\":\"str\",\"value\":\"he\\u2028llo\\u2029!\"},"
+ "{\"name\":null,\"type\":\"int\",\"value\":42},"
+ "{\"name\":null,\"type\":\"null\",\"value\":null}]";
} else {
expectedNLjson = null;
fail("unexpected namedListStyle=" + namedListStyle);
}
jsonEq("{" + expectedNLjson + ",\"byte\":-3,\"short\":-4,\"bytes\":\"YWJj\"}", buf.toString());
req.close();
}
@Test
public void testJSONSolrDocument() throws Exception {
SolrQueryRequest req =
req(
CommonParams.WT, "json",
CommonParams.FL, "id,score,_children_,path");
SolrQueryResponse rsp = new SolrQueryResponse();
JSONResponseWriter w = new JSONResponseWriter();
ReturnFields returnFields = new SolrReturnFields(req);
rsp.setReturnFields(returnFields);
StringWriter buf = new StringWriter();
SolrDocument childDoc = new SolrDocument();
childDoc.addField("id", "2");
childDoc.addField("score", "0.4");
childDoc.addField("path", Arrays.asList("a>b", "a>b>c"));
SolrDocumentList childList = new SolrDocumentList();
childList.setNumFound(1);
childList.setStart(0);
childList.add(childDoc);
SolrDocument solrDoc = new SolrDocument();
solrDoc.addField("id", "1");
solrDoc.addField("subject", "hello2");
solrDoc.addField("title", "hello3");
solrDoc.addField("score", "0.7");
solrDoc.setField("_children_", childList);
SolrDocumentList list = new SolrDocumentList();
list.setNumFound(1);
list.setStart(0);
list.setMaxScore(0.7f);
list.add(solrDoc);
rsp.addResponse(list);
w.write(buf, req, rsp);
String result = buf.toString();
assertFalse(
"response contains unexpected fields: " + result,
result.contains("hello") || result.contains("\"subject\"") || result.contains("\"title\""));
assertTrue(
"response doesn't contain expected fields: " + result,
result.contains("\"id\"") && result.contains("\"score\"") && result.contains("_children_"));
String expectedResult =
"{'response':{'numFound':1,'start':0,'maxScore':0.7, 'numFoundExact':true,'docs':[{'id':'1', 'score':'0.7',"
+ " '_children_':{'numFound':1,'start':0,'numFoundExact':true,'docs':[{'id':'2', 'score':'0.4', 'path':['a>b', 'a>b>c']}] }}] }}";
String error = JSONTestUtil.match(result, "==" + expectedResult);
assertNull("response validation failed with error: " + error, error);
req.close();
}
@Test
public void testArrntvWriterOverridesAllWrites() {
// List rather than Set because two not-overridden methods could share name but not signature
final List<String> methodsExpectedNotOverridden = new ArrayList<>(14);
methodsExpectedNotOverridden.add("writeResponse");
methodsExpectedNotOverridden.add("writeKey");
methodsExpectedNotOverridden.add("writeNamedListAsMapMangled");
methodsExpectedNotOverridden.add("writeNamedListAsMapWithDups");
methodsExpectedNotOverridden.add("writeNamedListAsArrMap");
methodsExpectedNotOverridden.add("writeNamedListAsArrArr");
methodsExpectedNotOverridden.add("writeNamedListAsFlat");
methodsExpectedNotOverridden.add("writeEndDocumentList");
methodsExpectedNotOverridden.add("writeMapOpener");
methodsExpectedNotOverridden.add("writeMapSeparator");
methodsExpectedNotOverridden.add("writeMapCloser");
methodsExpectedNotOverridden.add(
"public default void org.apache.solr.common.util.JsonTextWriter.writeArray(java.lang.String,java.util.List,boolean) throws java.io.IOException");
methodsExpectedNotOverridden.add("writeArrayOpener");
methodsExpectedNotOverridden.add("writeArraySeparator");
methodsExpectedNotOverridden.add("writeArrayCloser");
methodsExpectedNotOverridden.add(
"public default void org.apache.solr.common.util.JsonTextWriter.writeMap(org.apache.solr.common.MapWriter) throws java.io.IOException");
methodsExpectedNotOverridden.add(
"public default void org.apache.solr.common.util.JsonTextWriter.writeIterator(org.apache.solr.common.IteratorWriter) throws java.io.IOException");
methodsExpectedNotOverridden.add(
"public default void org.apache.solr.common.util.JsonTextWriter.writeJsonIter(java.util.Iterator,boolean) throws java.io.IOException");
final Class<?> subClass = JSONResponseWriter.ArrayOfNameTypeValueJSONWriter.class;
final Class<?> superClass = subClass.getSuperclass();
List<Method> allSuperClassMethods = new ArrayList<>();
for (Method method : superClass.getDeclaredMethods()) allSuperClassMethods.add(method);
for (Method method : JsonTextWriter.class.getDeclaredMethods())
allSuperClassMethods.add(method);
for (final Method superClassMethod : allSuperClassMethods) {
final String methodName = superClassMethod.getName();
final String methodFullName = superClassMethod.toString();
if (!methodName.startsWith("write")) continue;
final int modifiers = superClassMethod.getModifiers();
if (Modifier.isFinal(modifiers)) continue;
if (Modifier.isStatic(modifiers)) continue;
if (Modifier.isPrivate(modifiers)) continue;
final boolean expectOverridden =
!methodsExpectedNotOverridden.contains(methodName)
&& !methodsExpectedNotOverridden.contains(methodFullName);
try {
final Method subClassMethod = getDeclaredMethodInClasses(superClassMethod, subClass);
if (expectOverridden) {
assertEquals(
"getReturnType() difference",
superClassMethod.getReturnType(),
subClassMethod.getReturnType());
} else {
fail(subClass + " must not override '" + superClassMethod + "'");
}
} catch (NoSuchMethodException e) {
if (expectOverridden) {
fail(subClass + " needs to override '" + superClassMethod + "'");
} else {
assertTrue(
methodName + " not found in remaining " + methodsExpectedNotOverridden,
methodsExpectedNotOverridden.remove(methodName)
|| methodsExpectedNotOverridden.remove(methodFullName));
}
}
}
assertTrue(
"methodsExpected NotOverridden but NotFound instead: " + methodsExpectedNotOverridden,
methodsExpectedNotOverridden.isEmpty());
}
@Test
public void testArrntvWriterLacksMethodsOfItsOwn() {
final Class<?> subClass = JSONResponseWriter.ArrayOfNameTypeValueJSONWriter.class;
final Class<?> superClass = subClass.getSuperclass();
// ArrayOfNamedValuePairJSONWriter is a simple sub-class
// which should have (almost) no methods of its own
for (final Method subClassMethod : subClass.getDeclaredMethods()) {
// only own private method of its own
if (subClassMethod.getName().equals("ifNeededWriteTypeAndValueKey")) continue;
try {
final Method superClassMethod =
getDeclaredMethodInClasses(subClassMethod, superClass, JsonTextWriter.class);
assertEquals(
"getReturnType() difference",
subClassMethod.getReturnType(),
superClassMethod.getReturnType());
} catch (NoSuchMethodException e) {
fail(subClass + " should not have '" + subClassMethod + "' method of its own");
}
}
}
private Method getDeclaredMethodInClasses(Method subClassMethod, Class<?>... classes)
throws NoSuchMethodException {
for (int i = 0; i < classes.length; i++) {
Class<?> klass = classes[i];
try {
return klass.getDeclaredMethod(
subClassMethod.getName(), subClassMethod.getParameterTypes());
} catch (NoSuchMethodException e) {
if (i == classes.length - 1) throw e;
}
}
throw new NoSuchMethodException(subClassMethod.toString());
}
@Test
public void testConstantsUnchanged() {
assertEquals("json.nl", JSONWriter.JSON_NL_STYLE);
assertEquals("map", JSONWriter.JSON_NL_MAP);
assertEquals("flat", JSONWriter.JSON_NL_FLAT);
assertEquals("arrarr", JSONWriter.JSON_NL_ARROFARR);
assertEquals("arrmap", JSONWriter.JSON_NL_ARROFMAP);
assertEquals("arrntv", JSONWriter.JSON_NL_ARROFNTV);
assertEquals("json.wrf", JSONWriter.JSON_WRAPPER_FUNCTION);
}
@Test
public void testWfrJacksonJsonWriter() throws IOException {
SolrQueryRequest req = req("wt", "json", JSONWriter.JSON_WRAPPER_FUNCTION, "testFun");
SolrQueryResponse rsp = new SolrQueryResponse();
rsp.add("param0", "v0");
rsp.add("param1", 42);
JacksonJsonWriter w = new JacksonJsonWriter();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
w.write(baos, req, rsp);
String received = new String(baos.toByteArray(), StandardCharsets.UTF_8);
String expected = "testFun( {\n \"param0\":\"v0\",\n \"param1\":42\n} )";
jsonEq(expected, received);
req.close();
}
@Test
public void testWfrJSONWriter() throws IOException {
SolrQueryRequest req = req("wt", "json", JSONWriter.JSON_WRAPPER_FUNCTION, "testFun");
SolrQueryResponse rsp = new SolrQueryResponse();
rsp.add("param0", "v0");
rsp.add("param1", 42);
JSONResponseWriter w = new JSONResponseWriter();
StringWriter buf = new StringWriter();
w.write(buf, req, rsp);
String expected = "testFun({\n \"param0\":\"v0\",\n \"param1\":42})";
jsonEq(expected, buf.toString());
req.close();
}
@Test
public void testResponseValuesProperlyQuoted() throws Exception {
assertU(
adoc(
"id",
"1",
"name",
"John Doe",
"cat",
"foo\"b'ar",
"uuid",
"6e2fb55b-dd42-4e2d-84ca-71a599403aa3",
"bsto",
"true",
"isto",
"42",
"amount",
"100,USD",
"price",
"3.14",
"severity",
"High",
"dateRange",
"[2024-03-01 TO 2024-03-31]",
"timestamp",
"2024-03-20T12:34:56Z"));
assertU(commit());
String expected =
"{\n"
+ " \"response\":{\n"
+ " \"numFound\":1,\n"
+ " \"start\":0,\n"
+ " \"numFoundExact\":true,\n"
+ " \"docs\":[{\n"
+ " \"id\":\"1\",\n"
+ " \"name\":[\"John Doe\"],\n"
+ " \"cat\":[\"foo\\\"b'ar\"],\n"
+ " \"uuid\":[\"6e2fb55b-dd42-4e2d-84ca-71a599403aa3\"],\n"
+ " \"bsto\":[true],\n"
+ " \"isto\":[42],\n"
+ " \"amount\":\"100,USD\",\n"
+ " \"price\":3.14,\n"
+ " \"severity\":\"High\",\n"
+ " \"dateRange\":[\"[2024-03-01 TO 2024-03-31]\"],\n"
+ " \"timestamp\":\"2024-03-20T12:34:56Z\"\n"
+ " }]\n"
+ " }\n"
+ "}";
String fl = "id,name,cat,uuid,bsto,isto,amount,price,severity,dateRange,timestamp";
var req = req("q", "id:*", "fl", fl, "wt", "json", "omitHeader", "true");
try (req) {
String response = h.query("/select", req);
jsonEq(expected, response);
}
}
}