/*
* 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.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.solr.common.IteratorWriter;
import org.apache.solr.common.MapWriter.EntryWriter;
import org.apache.solr.common.PushWriter;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.MapWriter;
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;
import org.apache.solr.search.ReturnFields;
/**
*
*/
public class JSONResponseWriter implements QueryResponseWriter {
public static String CONTENT_TYPE_JSON_UTF8 = "application/json; charset=UTF-8";
private String contentType = CONTENT_TYPE_JSON_UTF8;
@Override
public void init(NamedList namedList) {
String contentType = (String) namedList.get("content-type");
if (contentType != null) {
this.contentType = contentType;
}
}
@Override
public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
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_ARROFNTV)) {
w = new ArrayOfNameTypeValueJSONWriter(
writer, req, rsp, wrapperFunction, namedListStyle, true);
} else {
w = new JSONWriter(
writer, req, rsp, wrapperFunction, namedListStyle);
}
try {
w.writeResponse();
} finally {
w.close();
}
}
@Override
public String getContentType(SolrQueryRequest request, SolrQueryResponse response) {
return contentType;
}
public static PushWriter getPushWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) {
return new JSONWriter(writer, req, rsp);
}
}
class JSONWriter extends TextResponseWriter {
protected String wrapperFunction;
final protected String namedListStyle;
static final String JSON_NL_STYLE="json.nl";
static final int JSON_NL_STYLE_COUNT = 5; // for use by JSONWriterTest
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_ARROFNTV="arrntv";
static final String JSON_WRAPPER_FUNCTION="json.wrf";
public JSONWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) {
this(writer, req, rsp,
req.getParams().get(JSON_WRAPPER_FUNCTION),
req.getParams().get(JSON_NL_STYLE, JSON_NL_FLAT).intern());
}
public JSONWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp,
String wrapperFunction, String namedListStyle) {
super(writer, req, rsp);
this.wrapperFunction = wrapperFunction;
this.namedListStyle = namedListStyle;
}
public void writeResponse() throws IOException {
if(wrapperFunction!=null) {
writer.write(wrapperFunction + "(");
}
writeNamedList(null, rsp.getValues());
if(wrapperFunction!=null) {
writer.write(')');
}
writer.write('\n'); // ending with a newline looks much better from the command line
}
protected void writeKey(String fname, boolean needsEscaping) throws IOException {
writeStr(null, fname, needsEscaping);
writer.write(':');
}
/** Represents a NamedList directly as a JSON Object (essentially a Map)
* Map null to "" and name mangle any repeated keys to avoid repeats in the
* output.
*/
protected void writeNamedListAsMapMangled(String name, NamedList val) throws IOException {
int sz = val.size();
writeMapOpener(sz);
incLevel();
// In JSON objects (maps) we can't have null keys or duplicates...
// map null to "" and append a qualifier to duplicates.
//
// a=123,a=456 will be mapped to {a=1,a__1=456}
// Disad: this is ambiguous since a real key could be called a__1
//
// Another possible mapping could aggregate multiple keys to an array:
// a=123,a=456 maps to a=[123,456]
// Disad: this is ambiguous with a real single value that happens to be an array
//
// Both of these mappings have ambiguities.
HashMap<String,Integer> repeats = new HashMap<>(4);
boolean first=true;
for (int i=0; i<sz; i++) {
String key = val.getName(i);
if (key==null) key="";
if (first) {
first=false;
repeats.put(key,0);
} else {
writeMapSeparator();
Integer repeatCount = repeats.get(key);
if (repeatCount==null) {
repeats.put(key,0);
} else {
String newKey = key;
int newCount = repeatCount;
do { // avoid generated key clashing with a real key
newKey = key + ' ' + (++newCount);
repeatCount = repeats.get(newKey);
} while (repeatCount != null);
repeats.put(key,newCount);
key = newKey;
}
}
indent();
writeKey(key, true);
writeVal(key,val.getVal(i));
}
decLevel();
writeMapCloser();
}
/** Represents a NamedList directly as a JSON Object (essentially a Map)
* repeating any keys if they are repeated in the NamedList.
* null key is mapped to "".
*/
// NamedList("a"=1,"bar"="foo",null=3,null=null) => {"a":1,"bar":"foo","":3,"":null}
protected void writeNamedListAsMapWithDups(String name, NamedList val) throws IOException {
int sz = val.size();
writeMapOpener(sz);
incLevel();
for (int i=0; i<sz; i++) {
if (i!=0) {
writeMapSeparator();
}
String key = val.getName(i);
if (key==null) key="";
indent();
writeKey(key, true);
writeVal(key,val.getVal(i));
}
decLevel();
writeMapCloser();
}
// Represents a NamedList directly as an array of JSON objects...
// NamedList("a"=1,"b"=2,null=3,null=null) => [{"a":1},{"b":2},3,null]
protected void writeNamedListAsArrMap(String name, NamedList val) throws IOException {
int sz = val.size();
indent();
writeArrayOpener(sz);
incLevel();
boolean first=true;
for (int i=0; i<sz; i++) {
String key = val.getName(i);
if (first) {
first=false;
} else {
writeArraySeparator();
}
indent();
if (key==null) {
writeVal(null,val.getVal(i));
} else {
writeMapOpener(1);
writeKey(key, true);
writeVal(key,val.getVal(i));
writeMapCloser();
}
}
decLevel();
writeArrayCloser();
}
// Represents a NamedList directly as an array of JSON objects...
// NamedList("a"=1,"b"=2,null=3,null=null) => [["a",1],["b",2],[null,3],[null,null]]
protected void writeNamedListAsArrArr(String name, NamedList val) throws IOException {
int sz = val.size();
indent();
writeArrayOpener(sz);
incLevel();
boolean first=true;
for (int i=0; i<sz; i++) {
String key = val.getName(i);
if (first) {
first=false;
} else {
writeArraySeparator();
}
indent();
/*** if key is null, just write value???
if (key==null) {
writeVal(null,val.getVal(i));
} else {
***/
writeArrayOpener(1);
incLevel();
if (key==null) {
writeNull(null);
} else {
writeStr(null, key, true);
}
writeArraySeparator();
writeVal(key,val.getVal(i));
decLevel();
writeArrayCloser();
}
decLevel();
writeArrayCloser();
}
// Represents a NamedList directly as an array with keys/values
// interleaved.
// NamedList("a"=1,"b"=2,null=3,null=null) => ["a",1,"b",2,null,3,null,null]
protected void writeNamedListAsFlat(String name, NamedList val) throws IOException {
int sz = val.size();
writeArrayOpener(sz*2);
incLevel();
for (int i=0; i<sz; i++) {
if (i!=0) {
writeArraySeparator();
}
String key = val.getName(i);
indent();
if (key==null) {
writeNull(null);
} else {
writeStr(null, key, true);
}
writeArraySeparator();
writeVal(key, val.getVal(i));
}
decLevel();
writeArrayCloser();
}
@Override
public void writeNamedList(String name, NamedList val) throws IOException {
if (val instanceof SimpleOrderedMap) {
writeNamedListAsMapWithDups(name,val);
} else if (namedListStyle==JSON_NL_FLAT) {
writeNamedListAsFlat(name,val);
} else if (namedListStyle==JSON_NL_MAP){
writeNamedListAsMapWithDups(name,val);
} else if (namedListStyle==JSON_NL_ARROFARR) {
writeNamedListAsArrArr(name,val);
} else if (namedListStyle==JSON_NL_ARROFMAP) {
writeNamedListAsArrMap(name,val);
} else if (namedListStyle==JSON_NL_ARROFNTV) {
throw new UnsupportedOperationException(namedListStyle
+ " namedListStyle must only be used with "+ArrayOfNameTypeValueJSONWriter.class.getSimpleName());
}
}
@Override
public void writeSolrDocument(String name, SolrDocument doc, ReturnFields returnFields, int idx) throws IOException {
if( idx > 0 ) {
writeArraySeparator();
}
indent();
writeMapOpener(doc.size());
incLevel();
boolean first=true;
for (String fname : doc.getFieldNames()) {
if (returnFields!= null && !returnFields.wantsField(fname)) {
continue;
}
if (first) {
first=false;
}
else {
writeMapSeparator();
}
indent();
writeKey(fname, true);
Object val = doc.getFieldValue(fname);
// SolrDocument will now have multiValued fields represented as a Collection,
// even if only a single value is returned for this document.
if (val instanceof List) {
// shortcut this common case instead of going through writeVal again
writeArray(name,((Iterable)val).iterator());
} else {
writeVal(fname, val);
}
}
if(doc.hasChildDocuments()) {
if(first == false) {
writeMapSeparator();
indent();
}
writeKey("_childDocuments_", true);
writeArrayOpener(doc.getChildDocumentCount());
List<SolrDocument> childDocs = doc.getChildDocuments();
for(int i=0; i<childDocs.size(); i++) {
writeSolrDocument(null, childDocs.get(i), null, i);
}
writeArrayCloser();
}
decLevel();
writeMapCloser();
}
@Override
public void writeStartDocumentList(String name,
long start, int size, long numFound, Float maxScore) throws IOException
{
writeMapOpener((maxScore==null) ? 3 : 4);
incLevel();
writeKey("numFound",false);
writeLong(null,numFound);
writeMapSeparator();
writeKey("start",false);
writeLong(null,start);
if (maxScore!=null) {
writeMapSeparator();
writeKey("maxScore",false);
writeFloat(null,maxScore);
}
writeMapSeparator();
// indent();
writeKey("docs",false);
writeArrayOpener(size);
incLevel();
}
@Override
public void writeEndDocumentList() throws IOException
{
decLevel();
writeArrayCloser();
decLevel();
indent();
writeMapCloser();
}
//
// Data structure tokens
// NOTE: a positive size paramater indicates the number of elements
// contained in an array or map, a negative value indicates
// that the size could not be reliably determined.
//
public void writeMapOpener(int size) throws IOException, IllegalArgumentException {
writer.write('{');
}
public void writeMapSeparator() throws IOException {
writer.write(',');
}
public void writeMapCloser() throws IOException {
writer.write('}');
}
public void writeArrayOpener(int size) throws IOException, IllegalArgumentException {
writer.write('[');
}
public void writeArraySeparator() throws IOException {
writer.write(',');
}
public void writeArrayCloser() throws IOException {
writer.write(']');
}
@Override
public void writeStr(String name, String val, boolean needsEscaping) throws IOException {
// it might be more efficient to use a stringbuilder or write substrings
// if writing chars to the stream is slow.
if (needsEscaping) {
/* http://www.ietf.org/internet-drafts/draft-crockford-jsonorg-json-04.txt
All Unicode characters may be placed within
the quotation marks except for the characters which must be
escaped: quotation mark, reverse solidus, and the control
characters (U+0000 through U+001F).
*/
writer.write('"');
for (int i=0; i<val.length(); i++) {
char ch = val.charAt(i);
if ((ch > '#' && ch != '\\' && ch < '\u2028') || ch == ' ') { // fast path
writer.write(ch);
continue;
}
switch(ch) {
case '"':
case '\\':
writer.write('\\');
writer.write(ch);
break;
case '\r': writer.write('\\'); writer.write('r'); break;
case '\n': writer.write('\\'); writer.write('n'); break;
case '\t': writer.write('\\'); writer.write('t'); break;
case '\b': writer.write('\\'); writer.write('b'); break;
case '\f': writer.write('\\'); writer.write('f'); break;
case '\u2028': // fallthrough
case '\u2029':
unicodeEscape(writer,ch);
break;
// case '/':
default: {
if (ch <= 0x1F) {
unicodeEscape(writer,ch);
} else {
writer.write(ch);
}
}
}
}
writer.write('"');
} else {
writer.write('"');
writer.write(val);
writer.write('"');
}
}
@Override
public void writeIterator(IteratorWriter val) throws IOException {
writeArrayOpener(-1);
incLevel();
val.writeIter(new IteratorWriter.ItemWriter() {
boolean first = true;
@Override
public IteratorWriter.ItemWriter add(Object o) throws IOException {
if (!first) {
JSONWriter.this.indent();
JSONWriter.this.writeArraySeparator();
}
JSONWriter.this.writeVal(null, o);
first = false;
return this;
}
});
decLevel();
writeArrayCloser();
}
@Override
public void writeMap(MapWriter val)
throws IOException {
writeMapOpener(-1);
incLevel();
val.writeMap(new EntryWriter() {
boolean isFirst = true;
@Override
public EntryWriter put(String k, Object v) throws IOException {
if (isFirst) {
isFirst = false;
} else {
JSONWriter.this.writeMapSeparator();
}
if (doIndent) JSONWriter.this.indent();
JSONWriter.this.writeKey(k, true);
JSONWriter.this.writeVal(k, v);
return this;
}
});
decLevel();
writeMapCloser();
}
@Override
public void writeMap(String name, Map val, boolean excludeOuter, boolean isFirstVal) throws IOException {
if (!excludeOuter) {
writeMapOpener(val.size());
incLevel();
isFirstVal=true;
}
boolean doIndent = excludeOuter || val.size() > 1;
for (Map.Entry entry : (Set<Map.Entry>)val.entrySet()) {
Object e = entry.getKey();
String k = e==null ? "" : e.toString();
Object v = entry.getValue();
if (isFirstVal) {
isFirstVal=false;
} else {
writeMapSeparator();
}
if (doIndent) indent();
writeKey(k,true);
writeVal(k,v);
}
if (!excludeOuter) {
decLevel();
writeMapCloser();
}
}
@Override
public void writeArray(String name, List l) throws IOException {
writeArrayOpener(l.size());
writeJsonIter(l.iterator());
writeArrayCloser();
}
@Override
public void writeArray(String name, Iterator val) throws IOException {
writeArrayOpener(-1); // no trivial way to determine array size
writeJsonIter(val);
writeArrayCloser();
}
private void writeJsonIter(Iterator val) throws IOException {
incLevel();
boolean first=true;
while( val.hasNext() ) {
if( !first ) indent();
writeVal(null, val.next());
if( val.hasNext() ) {
writeArraySeparator();
}
first=false;
}
decLevel();
}
//
// Primitive types
//
@Override
public void writeNull(String name) throws IOException {
writer.write("null");
}
@Override
public void writeInt(String name, String val) throws IOException {
writer.write(val);
}
@Override
public void writeLong(String name, String val) throws IOException {
writer.write(val);
}
@Override
public void writeBool(String name, String val) throws IOException {
writer.write(val);
}
@Override
public void writeFloat(String name, String val) throws IOException {
writer.write(val);
}
@Override
public void writeDouble(String name, String val) throws IOException {
writer.write(val);
}
@Override
public void writeDate(String name, String val) throws IOException {
writeStr(name, val, false);
}
private static char[] hexdigits = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
protected static void unicodeEscape(Appendable out, int ch) throws IOException {
out.append('\\');
out.append('u');
out.append(hexdigits[(ch>>>12) ]);
out.append(hexdigits[(ch>>>8) & 0xf]);
out.append(hexdigits[(ch>>>4) & 0xf]);
out.append(hexdigits[(ch) & 0xf]);
}
}
/**
* Writes NamedLists directly as an array of NameTypeValue JSON objects...
* NamedList("a"=1,"b"=null,null=3,null=null) =>
* [{"name":"a","type":"int","value":1},
* {"name":"b","type":"null","value":null},
* {"name":null,"type":"int","value":3},
* {"name":null,"type":"null","value":null}]
* NamedList("a"=1,"bar"="foo",null=3.4f) =>
* [{"name":"a","type":"int","value":1},
* {"name":"bar","type":"str","value":"foo"},
* {"name":null,"type":"float","value":3.4}]
*/
class ArrayOfNameTypeValueJSONWriter extends JSONWriter {
protected boolean writeTypeAndValueKey = false;
private final boolean writeNullName;
public ArrayOfNameTypeValueJSONWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp,
String wrapperFunction, String namedListStyle, boolean writeNullName) {
super(writer, req, rsp, wrapperFunction, namedListStyle);
this.writeNullName = writeNullName;
}
@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<sz; i++) {
if (first) {
first = false;
} else {
writeArraySeparator();
}
indent();
final String elementName = val.getName(i);
final Object elementVal = val.getVal(i);
/*
* JSONWriter's writeNamedListAsArrMap turns NamedList("bar"="foo") into [{"foo":"bar"}]
* but we here wish to turn it into [ {"name":"bar","type":"str","value":"foo"} ] instead.
*
* So first we write the <code>{"name":"bar",</code> portion ...
*/
writeMapOpener(-1);
if (elementName != null || writeNullName) {
writeKey("name", false);
writeVal("name", elementName);
writeMapSeparator();
}
/*
* ... and then we write the <code>"type":"str","value":"foo"}</code> portion.
*/
writeTypeAndValueKey = true;
writeVal(null, elementVal); // passing null since writeVal doesn't actually use name (and we already wrote elementName above)
if (writeTypeAndValueKey) {
throw new RuntimeException("writeTypeAndValueKey should have been reset to false by writeVal('"+elementName+"','"+elementVal+"')");
}
writeMapCloser();
}
decLevel();
writeArrayCloser();
}
protected void ifNeededWriteTypeAndValueKey(String type) throws IOException {
if (writeTypeAndValueKey) {
writeTypeAndValueKey = false;
writeKey("type", false);
writeVal("type", type);
writeMapSeparator();
writeKey("value", false);
}
}
@Override
public void writeInt(String name, String val) throws IOException {
ifNeededWriteTypeAndValueKey("int");
super.writeInt(name, val);
}
@Override
public void writeLong(String name, String val) throws IOException {
ifNeededWriteTypeAndValueKey("long");
super.writeLong(name, val);
}
@Override
public void writeFloat(String name, String val) throws IOException {
ifNeededWriteTypeAndValueKey("float");
super.writeFloat(name, val);
}
@Override
public void writeDouble(String name, String val) throws IOException {
ifNeededWriteTypeAndValueKey("double");
super.writeDouble(name, val);
}
@Override
public void writeBool(String name, String val) throws IOException {
ifNeededWriteTypeAndValueKey("bool");
super.writeBool(name, val);
}
@Override
public void writeDate(String name, String val) throws IOException {
ifNeededWriteTypeAndValueKey("date");
super.writeDate(name, val);
}
@Override
public void writeStr(String name, String val, boolean needsEscaping) throws IOException {
ifNeededWriteTypeAndValueKey("str");
super.writeStr(name, val, needsEscaping);
}
@Override
public void writeSolrDocument(String name, SolrDocument doc, ReturnFields returnFields, int idx) throws IOException {
ifNeededWriteTypeAndValueKey("doc");
super.writeSolrDocument(name, doc, returnFields, idx);
}
@Override
public void writeStartDocumentList(String name, long start, int size, long numFound, Float maxScore) throws IOException {
ifNeededWriteTypeAndValueKey("doclist");
super.writeStartDocumentList(name, start, size, numFound, maxScore);
}
@Override
public void writeMap(String name, Map val, boolean excludeOuter, boolean isFirstVal) throws IOException {
ifNeededWriteTypeAndValueKey("map");
super.writeMap(name, val, excludeOuter, isFirstVal);
}
@Override
public void writeArray(String name, Iterator val) throws IOException {
ifNeededWriteTypeAndValueKey("array");
super.writeArray(name, val);
}
@Override
public void writeNull(String name) throws IOException {
ifNeededWriteTypeAndValueKey("null");
super.writeNull(name);
}
}
abstract class NaNFloatWriter extends JSONWriter {
abstract protected String getNaN();
abstract protected String getInf();
public NaNFloatWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) {
super(writer, req, rsp);
}
@Override
public void writeFloat(String name, float val) throws IOException {
if (Float.isNaN(val)) {
writer.write(getNaN());
} else if (Float.isInfinite(val)) {
if (val < 0.0f)
writer.write('-');
writer.write(getInf());
} else {
writeFloat(name, Float.toString(val));
}
}
@Override
public void writeDouble(String name, double val) throws IOException {
if (Double.isNaN(val)) {
writer.write(getNaN());
} else if (Double.isInfinite(val)) {
if (val < 0.0)
writer.write('-');
writer.write(getInf());
} else {
writeDouble(name, Double.toString(val));
}
}
}