diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 2f464167903..09bf0078a67 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -86,6 +86,9 @@ New Features Example: { type:terms, field:category, filter:"user:yonik" } (yonik) +* SOLR-9442: Adds Array of NamedValuePair (json.nl=arrnvp) style to JSONResponseWriter. + (Jonny Marks, Christine Poerschke) + Optimizations ---------------------- * SOLR-9704: Facet Module / JSON Facet API: Optimize blockChildren facets that have diff --git a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java index cd6648bf111..ad128d2465e 100644 --- a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java +++ b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java @@ -26,6 +26,7 @@ import java.util.Set; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.params.CommonParams; +import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.SimpleOrderedMap; import org.apache.solr.request.SolrQueryRequest; @@ -50,7 +51,19 @@ public class JSONResponseWriter implements QueryResponseWriter { @Override public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException { - JSONWriter w = new JSONWriter(writer, req, rsp); + final SolrParams params = req.getParams(); + final String wrapperFunction = params.get(JSONWriter.JSON_WRAPPER_FUNCTION); + final String namedListStyle = params.get(JSONWriter.JSON_NL_STYLE, JSONWriter.JSON_NL_FLAT).intern(); + + final JSONWriter w; + if (namedListStyle.equals(JSONWriter.JSON_NL_ARROFNVP)) { + w = new ArrayOfNamedValuePairJSONWriter( + writer, req, rsp, wrapperFunction, namedListStyle); + } else { + w = new JSONWriter( + writer, req, rsp, wrapperFunction, namedListStyle); + } + try { w.writeResponse(); } finally { @@ -66,13 +79,14 @@ public class JSONResponseWriter implements QueryResponseWriter { class JSONWriter extends TextResponseWriter { protected String wrapperFunction; - final private String namedListStyle; + final protected String namedListStyle; static final String JSON_NL_STYLE="json.nl"; static final String JSON_NL_MAP="map"; static final String JSON_NL_FLAT="flat"; static final String JSON_NL_ARROFARR="arrarr"; static final String JSON_NL_ARROFMAP="arrmap"; + static final String JSON_NL_ARROFNVP="arrnvp"; static final String JSON_WRAPPER_FUNCTION="json.wrf"; public JSONWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) { @@ -306,6 +320,9 @@ class JSONWriter extends TextResponseWriter { writeNamedListAsArrArr(name,val); } else if (namedListStyle==JSON_NL_ARROFMAP) { writeNamedListAsArrMap(name,val); + } else if (namedListStyle==JSON_NL_ARROFNVP) { + throw new UnsupportedOperationException(namedListStyle + + " namedListStyle must only be used with "+ArrayOfNamedValuePairJSONWriter.class.getSimpleName()); } } @@ -588,6 +605,158 @@ class JSONWriter extends TextResponseWriter { } +/** + * Writes NamedLists directly as an array of NamedValuePair JSON objects... + * NamedList("a"=1,"b"=2,null=3) => [{"name":"a","int":1},{"name":"b","int":2},{"int":3}] + * NamedList("a"=1,"bar"="foo",null=3.4f) => [{"name":"a","int":1},{"name":"bar","str":"foo"},{"float":3.4}] + */ +class ArrayOfNamedValuePairJSONWriter extends JSONWriter { + private boolean writeTypeAsKey = false; + + public ArrayOfNamedValuePairJSONWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp, + String wrapperFunction, String namedListStyle) { + super(writer, req, rsp, wrapperFunction, namedListStyle); + if (namedListStyle != JSON_NL_ARROFNVP) { + throw new UnsupportedOperationException(ArrayOfNamedValuePairJSONWriter.class.getSimpleName()+" must only be used with " + + JSON_NL_ARROFNVP + " style"); + } + } + + @Override + public void writeNamedList(String name, NamedList val) throws IOException { + + if (val instanceof SimpleOrderedMap) { + super.writeNamedList(name, val); + return; + } + + final int sz = val.size(); + indent(); + + writeArrayOpener(sz); + incLevel(); + + boolean first = true; + for (int i=0; i{"name":"bar", portion ... + */ + writeMapOpener(-1); + if (elementName != null) { + writeKey("name", false); + writeVal("name", elementName); + writeMapSeparator(); + } + + /* + * ... and then we write the "str":"foo"} portion. + */ + writeTypeAsKey = true; + writeVal(null, elementVal); // passing null since writeVal doesn't actually use name (and we already wrote elementName above) + if (writeTypeAsKey) { + throw new RuntimeException("writeTypeAsKey should have been reset to false by writeVal('"+elementName+"','"+elementVal+"')"); + } + writeMapCloser(); + } + + decLevel(); + writeArrayCloser(); + } + + private void ifNeededWriteTypeAsKey(String type) throws IOException { + if (writeTypeAsKey) { + writeTypeAsKey = false; + writeKey(type, false); + } + } + + @Override + public void writeInt(String name, String val) throws IOException { + ifNeededWriteTypeAsKey("int"); + super.writeInt(name, val); + } + + @Override + public void writeLong(String name, String val) throws IOException { + ifNeededWriteTypeAsKey("long"); + super.writeLong(name, val); + } + + @Override + public void writeFloat(String name, String val) throws IOException { + ifNeededWriteTypeAsKey("float"); + super.writeFloat(name, val); + } + + @Override + public void writeDouble(String name, String val) throws IOException { + ifNeededWriteTypeAsKey("double"); + super.writeDouble(name, val); + } + + @Override + public void writeBool(String name, String val) throws IOException { + ifNeededWriteTypeAsKey("bool"); + super.writeBool(name, val); + } + + @Override + public void writeDate(String name, String val) throws IOException { + ifNeededWriteTypeAsKey("date"); + super.writeDate(name, val); + } + + @Override + public void writeStr(String name, String val, boolean needsEscaping) throws IOException { + ifNeededWriteTypeAsKey("str"); + super.writeStr(name, val, needsEscaping); + } + + @Override + public void writeSolrDocument(String name, SolrDocument doc, ReturnFields returnFields, int idx) throws IOException { + ifNeededWriteTypeAsKey("doc"); + super.writeSolrDocument(name, doc, returnFields, idx); + } + + @Override + public void writeStartDocumentList(String name, long start, int size, long numFound, Float maxScore) throws IOException { + ifNeededWriteTypeAsKey("doclist"); + super.writeStartDocumentList(name, start, size, numFound, maxScore); + } + + @Override + public void writeMap(String name, Map val, boolean excludeOuter, boolean isFirstVal) throws IOException { + ifNeededWriteTypeAsKey("map"); + super.writeMap(name, val, excludeOuter, isFirstVal); + } + + @Override + public void writeArray(String name, Iterator val) throws IOException { + ifNeededWriteTypeAsKey("array"); + super.writeArray(name, val); + } + + @Override + public void writeNull(String name) throws IOException { + ifNeededWriteTypeAsKey("null"); + super.writeNull(name); + } +} + abstract class NaNFloatWriter extends JSONWriter { abstract protected String getNaN(); diff --git a/solr/core/src/test/org/apache/solr/response/JSONWriterTest.java b/solr/core/src/test/org/apache/solr/response/JSONWriterTest.java index ad390cbb776..a61cff32aaf 100644 --- a/solr/core/src/test/org/apache/solr/response/JSONWriterTest.java +++ b/solr/core/src/test/org/apache/solr/response/JSONWriterTest.java @@ -18,7 +18,11 @@ package org.apache.solr.response; 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.List; import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; @@ -72,7 +76,8 @@ public class JSONWriterTest extends SolrTestCaseJ4 { @Test public void testJSON() throws IOException { - SolrQueryRequest req = req("wt","json","json.nl","arrarr"); + final String namedListStyle = (random().nextBoolean() ? JSONWriter.JSON_NL_ARROFARR : JSONWriter.JSON_NL_ARROFNVP); + SolrQueryRequest req = req("wt","json","json.nl",namedListStyle); SolrQueryResponse rsp = new SolrQueryResponse(); JSONResponseWriter w = new JSONResponseWriter(); @@ -87,7 +92,18 @@ public class JSONWriterTest extends SolrTestCaseJ4 { rsp.add("bytes", "abc".getBytes(StandardCharsets.UTF_8)); w.write(buf, req, rsp); - jsonEq("{\"nl\":[[\"data1\",\"he\\u2028llo\\u2029!\"],[null,42]],\"byte\":-3,\"short\":-4,\"bytes\":\"YWJj\"}", buf.toString()); + + final String expectedNLjson; + if (namedListStyle == JSONWriter.JSON_NL_ARROFARR) { + expectedNLjson = "\"nl\":[[\"data1\",\"he\\u2028llo\\u2029!\"],[null,42]]"; + } else if (namedListStyle == JSONWriter.JSON_NL_ARROFNVP) { + expectedNLjson = "\"nl\":[{\"name\":\"data1\",\"str\":\"he\\u2028llo\\u2029!\"},{\"int\":42}]"; + } else { + expectedNLjson = null; + fail("unexpected namedListStyle="+namedListStyle); + } + + jsonEq("{"+expectedNLjson+",\"byte\":-3,\"short\":-4,\"bytes\":\"YWJj\"}", buf.toString()); req.close(); } @@ -130,6 +146,87 @@ public class JSONWriterTest extends SolrTestCaseJ4 { req.close(); } + + @Test + public void testArrnvpWriterOverridesAllWrites() { + // List rather than Set because two not-overridden methods could share name but not signature + final List methodsExpectedNotOverriden = new ArrayList<>(14); + methodsExpectedNotOverriden.add("writeResponse"); + methodsExpectedNotOverriden.add("writeKey"); + methodsExpectedNotOverriden.add("writeNamedListAsMapMangled"); + methodsExpectedNotOverriden.add("writeNamedListAsMapWithDups"); + methodsExpectedNotOverriden.add("writeNamedListAsArrMap"); + methodsExpectedNotOverriden.add("writeNamedListAsArrArr"); + methodsExpectedNotOverriden.add("writeNamedListAsFlat"); + methodsExpectedNotOverriden.add("writeEndDocumentList"); + methodsExpectedNotOverriden.add("writeMapOpener"); + methodsExpectedNotOverriden.add("writeMapSeparator"); + methodsExpectedNotOverriden.add("writeMapCloser"); + methodsExpectedNotOverriden.add("writeArrayOpener"); + methodsExpectedNotOverriden.add("writeArraySeparator"); + methodsExpectedNotOverriden.add("writeArrayCloser"); + + final Class subClass = ArrayOfNamedValuePairJSONWriter.class; + final Class superClass = subClass.getSuperclass(); + + for (final Method superClassMethod : superClass.getDeclaredMethods()) { + final String methodName = superClassMethod.getName(); + 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 expectOverriden = !methodsExpectedNotOverriden.contains(methodName); + + try { + final Method subClassMethod = subClass.getDeclaredMethod( + superClassMethod.getName(), + superClassMethod.getParameterTypes()); + + if (expectOverriden) { + assertEquals("getReturnType() difference", + superClassMethod.getReturnType(), + subClassMethod.getReturnType()); + } else { + fail(subClass + " must not override '" + superClassMethod + "'"); + } + } catch (NoSuchMethodException e) { + if (expectOverriden) { + fail(subClass + " needs to override '" + superClassMethod + "'"); + } else { + assertTrue(methodName+" not found in remaining "+methodsExpectedNotOverriden, methodsExpectedNotOverriden.remove(methodName)); + } + } + } + + assertTrue("methodsExpected NotOverriden but NotFound instead: "+methodsExpectedNotOverriden, + methodsExpectedNotOverriden.isEmpty()); + } + + @Test + public void testArrnvpWriterLacksMethodsOfItsOwn() { + final Class subClass = ArrayOfNamedValuePairJSONWriter.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("ifNeededWriteTypeAsKey")) continue; + try { + final Method superClassMethod = superClass.getDeclaredMethod( + subClassMethod.getName(), + subClassMethod.getParameterTypes()); + + assertEquals("getReturnType() difference", + subClassMethod.getReturnType(), + superClassMethod.getReturnType()); + } catch (NoSuchMethodException e) { + fail(subClass + " should not have '" + subClassMethod + "' method of its own"); + } + } + } @Test public void testConstantsUnchanged() { @@ -138,6 +235,7 @@ public class JSONWriterTest extends SolrTestCaseJ4 { assertEquals("flat", JSONWriter.JSON_NL_FLAT); assertEquals("arrarr", JSONWriter.JSON_NL_ARROFARR); assertEquals("arrmap", JSONWriter.JSON_NL_ARROFMAP); + assertEquals("arrnvp", JSONWriter.JSON_NL_ARROFNVP); assertEquals("json.wrf", JSONWriter.JSON_WRAPPER_FUNCTION); }