001: /**
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */package org.apache.solr.request;
017:
018: import org.apache.lucene.document.Document;
019: import org.apache.lucene.document.Fieldable;
020: import org.apache.solr.schema.SchemaField;
021: import org.apache.solr.schema.TextField;
022: import org.apache.solr.search.DocIterator;
023: import org.apache.solr.search.DocList;
024: import org.apache.solr.search.SolrIndexSearcher;
025: import org.apache.solr.util.NamedList;
026: import org.apache.solr.util.SimpleOrderedMap;
027:
028: import java.io.IOException;
029: import java.io.Writer;
030: import java.util.*;
031:
032: /**
033: * @author yonik
034: * @version $Id$
035: */
036:
037: public class JSONResponseWriter implements QueryResponseWriter {
038: static String CONTENT_TYPE_JSON_UTF8 = "text/x-json; charset=UTF-8";
039:
040: public void init(NamedList n) {
041: }
042:
043: public void write(Writer writer, SolrQueryRequest req,
044: SolrQueryResponse rsp) throws IOException {
045: JSONWriter w = new JSONWriter(writer, req, rsp);
046: w.writeResponse();
047: }
048:
049: public String getContentType(SolrQueryRequest request,
050: SolrQueryResponse response) {
051: // using the text/plain allows this to be viewed in the browser easily
052: return CONTENT_TYPE_TEXT_UTF8;
053: }
054: }
055:
056: class JSONWriter extends TextResponseWriter {
057:
058: // cache the calendar instance in case we are writing many dates...
059: private Calendar cal;
060:
061: private String namedListStyle;
062: private String wrapperFunction;
063:
064: private static final String JSON_NL_STYLE = "json.nl";
065: private static final String JSON_NL_MAP = "map";
066: private static final String JSON_NL_FLAT = "flat";
067: private static final String JSON_NL_ARROFARR = "arrarr";
068: private static final String JSON_NL_ARROFMAP = "arrmap";
069: private static final String JSON_WRAPPER_FUNCTION = "json.wrf";
070:
071: public JSONWriter(Writer writer, SolrQueryRequest req,
072: SolrQueryResponse rsp) {
073: super (writer, req, rsp);
074: namedListStyle = req.getParams().get(JSON_NL_STYLE,
075: JSON_NL_FLAT).intern();
076: wrapperFunction = req.getParams().get(JSON_WRAPPER_FUNCTION);
077: }
078:
079: public void writeResponse() throws IOException {
080: if (wrapperFunction != null) {
081: writer.write(wrapperFunction + "(");
082: }
083: writeNamedList(null, rsp.getValues());
084: if (wrapperFunction != null) {
085: writer.write(")");
086: }
087: }
088:
089: protected void writeKey(String fname, boolean needsEscaping)
090: throws IOException {
091: writeStr(null, fname, needsEscaping);
092: writer.write(':');
093: }
094:
095: /** Represents a NamedList directly as a JSON Object (essentially a Map)
096: * Map null to "" and name mangle any repeated keys to avoid repeats in the
097: * output.
098: */
099: protected void writeNamedListAsMapMangled(String name, NamedList val)
100: throws IOException {
101: int sz = val.size();
102: writer.write('{');
103: incLevel();
104:
105: // In JSON objects (maps) we can't have null keys or duplicates...
106: // map null to "" and append a qualifier to duplicates.
107: //
108: // a=123,a=456 will be mapped to {a=1,a__1=456}
109: // Disad: this is ambiguous since a real key could be called a__1
110: //
111: // Another possible mapping could aggregate multiple keys to an array:
112: // a=123,a=456 maps to a=[123,456]
113: // Disad: this is ambiguous with a real single value that happens to be an array
114: //
115: // Both of these mappings have ambiguities.
116: HashMap<String, Integer> repeats = new HashMap<String, Integer>(
117: 4);
118:
119: boolean first = true;
120: for (int i = 0; i < sz; i++) {
121: String key = val.getName(i);
122: if (key == null)
123: key = "";
124:
125: if (first) {
126: first = false;
127: repeats.put(key, 0);
128: } else {
129: writer.write(',');
130:
131: Integer repeatCount = repeats.get(key);
132: if (repeatCount == null) {
133: repeats.put(key, 0);
134: } else {
135: String newKey = key;
136: int newCount = repeatCount;
137: do { // avoid generated key clashing with a real key
138: newKey = key + ' ' + (++newCount);
139: repeatCount = repeats.get(newKey);
140: } while (repeatCount != null);
141:
142: repeats.put(key, newCount);
143: key = newKey;
144: }
145: }
146:
147: indent();
148: writeKey(key, true);
149: writeVal(key, val.getVal(i));
150: }
151:
152: decLevel();
153: writer.write('}');
154: }
155:
156: /** Represents a NamedList directly as a JSON Object (essentially a Map)
157: * repeating any keys if they are repeated in the NamedList. null is mapped
158: * to "".
159: */
160: protected void writeNamedListAsMapWithDups(String name,
161: NamedList val) throws IOException {
162: int sz = val.size();
163: writer.write('{');
164: incLevel();
165:
166: for (int i = 0; i < sz; i++) {
167: if (i != 0) {
168: writer.write(',');
169: }
170:
171: String key = val.getName(i);
172: if (key == null)
173: key = "";
174: indent();
175: writeKey(key, true);
176: writeVal(key, val.getVal(i));
177: }
178:
179: decLevel();
180: writer.write('}');
181: }
182:
183: // Represents a NamedList directly as an array of JSON objects...
184: // NamedList("a"=1,"b"=2,null=3) => [{"a":1},{"b":2},3]
185: protected void writeNamedListAsArrMap(String name, NamedList val)
186: throws IOException {
187: int sz = val.size();
188: indent();
189: writer.write('[');
190: incLevel();
191:
192: boolean first = true;
193: for (int i = 0; i < sz; i++) {
194: String key = val.getName(i);
195:
196: if (first) {
197: first = false;
198: } else {
199: writer.write(',');
200: }
201:
202: indent();
203:
204: if (key == null) {
205: writeVal(null, val.getVal(i));
206: } else {
207: writer.write('{');
208: writeKey(key, true);
209: writeVal(key, val.getVal(i));
210: writer.write('}');
211: }
212:
213: }
214:
215: decLevel();
216: writer.write(']');
217: }
218:
219: // Represents a NamedList directly as an array of JSON objects...
220: // NamedList("a"=1,"b"=2,null=3) => [["a",1],["b",2],[null,3]]
221: protected void writeNamedListAsArrArr(String name, NamedList val)
222: throws IOException {
223: int sz = val.size();
224: indent();
225: writer.write('[');
226: incLevel();
227:
228: boolean first = true;
229: for (int i = 0; i < sz; i++) {
230: String key = val.getName(i);
231:
232: if (first) {
233: first = false;
234: } else {
235: writer.write(',');
236: }
237:
238: indent();
239:
240: /*** if key is null, just write value???
241: if (key==null) {
242: writeVal(null,val.getVal(i));
243: } else {
244: ***/
245:
246: writer.write('[');
247: incLevel();
248: writeStr(null, key, true);
249: writer.write(',');
250: writeVal(key, val.getVal(i));
251: decLevel();
252: writer.write(']');
253:
254: }
255:
256: decLevel();
257: writer.write(']');
258: }
259:
260: // Represents a NamedList directly as an array with keys/values
261: // interleaved.
262: // NamedList("a"=1,"b"=2,null=3) => ["a",1,"b",2,null,3]
263: protected void writeNamedListAsFlat(String name, NamedList val)
264: throws IOException {
265: int sz = val.size();
266: writer.write('[');
267: incLevel();
268:
269: for (int i = 0; i < sz; i++) {
270: if (i != 0) {
271: writer.write(',');
272: }
273: String key = val.getName(i);
274: indent();
275: if (key == null) {
276: writeNull(null);
277: } else {
278: writeStr(null, key, true);
279: }
280: writer.write(',');
281: writeVal(key, val.getVal(i));
282: }
283:
284: decLevel();
285: writer.write(']');
286: }
287:
288: public void writeNamedList(String name, NamedList val)
289: throws IOException {
290: if (val instanceof SimpleOrderedMap) {
291: writeNamedListAsMapWithDups(name, val);
292: } else if (namedListStyle == JSON_NL_FLAT) {
293: writeNamedListAsFlat(name, val);
294: } else if (namedListStyle == JSON_NL_MAP) {
295: writeNamedListAsMapWithDups(name, val);
296: } else if (namedListStyle == JSON_NL_ARROFARR) {
297: writeNamedListAsArrArr(name, val);
298: } else if (namedListStyle == JSON_NL_ARROFMAP) {
299: writeNamedListAsArrMap(name, val);
300: }
301: }
302:
303: private static class MultiValueField {
304: final SchemaField sfield;
305: final ArrayList<Fieldable> fields;
306:
307: MultiValueField(SchemaField sfield, Fieldable firstVal) {
308: this .sfield = sfield;
309: this .fields = new ArrayList<Fieldable>(4);
310: this .fields.add(firstVal);
311: }
312: }
313:
314: public void writeDoc(String name, Collection<Fieldable> fields,
315: Set<String> returnFields, Map pseudoFields)
316: throws IOException {
317: writer.write('{');
318: incLevel();
319:
320: HashMap<String, MultiValueField> multi = new HashMap<String, MultiValueField>();
321:
322: boolean first = true;
323:
324: for (Fieldable ff : fields) {
325: String fname = ff.name();
326: if (returnFields != null && !returnFields.contains(fname)) {
327: continue;
328: }
329:
330: // if the field is multivalued, it may have other values further on... so
331: // build up a list for each multi-valued field.
332: SchemaField sf = schema.getField(fname);
333: if (sf.multiValued()) {
334: MultiValueField mf = multi.get(fname);
335: if (mf == null) {
336: mf = new MultiValueField(sf, ff);
337: multi.put(fname, mf);
338: } else {
339: mf.fields.add(ff);
340: }
341: } else {
342: // not multi-valued, so write it immediately.
343: if (first) {
344: first = false;
345: } else {
346: writer.write(',');
347: }
348: indent();
349: writeKey(fname, true);
350: sf.write(this , fname, ff);
351: }
352: }
353:
354: for (MultiValueField mvf : multi.values()) {
355: if (first) {
356: first = false;
357: } else {
358: writer.write(',');
359: }
360:
361: indent();
362: writeKey(mvf.sfield.getName(), true);
363:
364: boolean indentArrElems = false;
365: if (doIndent) {
366: // heuristic... TextField is probably the only field type likely to be long enough
367: // to warrant indenting individual values.
368: indentArrElems = (mvf.sfield.getType() instanceof TextField);
369: }
370:
371: writer.write('[');
372: boolean firstArrElem = true;
373: incLevel();
374:
375: for (Fieldable ff : mvf.fields) {
376: if (firstArrElem) {
377: firstArrElem = false;
378: } else {
379: writer.write(',');
380: }
381: if (indentArrElems)
382: indent();
383: mvf.sfield.write(this , null, ff);
384: }
385: writer.write(']');
386: decLevel();
387: }
388:
389: if (pseudoFields != null && pseudoFields.size() > 0) {
390: writeMap(null, pseudoFields, true, first);
391: }
392:
393: decLevel();
394: writer.write('}');
395: }
396:
397: // reusable map to store the "score" pseudo-field.
398: // if a Doc can ever contain another doc, this optimization would have to go.
399: private final HashMap scoreMap = new HashMap(1);
400:
401: public void writeDoc(String name, Document doc,
402: Set<String> returnFields, float score, boolean includeScore)
403: throws IOException {
404: Map other = null;
405: if (includeScore) {
406: other = scoreMap;
407: scoreMap.put("score", score);
408: }
409: writeDoc(name, (List<Fieldable>) (doc.getFields()),
410: returnFields, other);
411: }
412:
413: public void writeDocList(String name, DocList ids,
414: Set<String> fields, Map otherFields) throws IOException {
415: boolean includeScore = false;
416: if (fields != null) {
417: includeScore = fields.contains("score");
418: if (fields.size() == 0
419: || (fields.size() == 1 && includeScore)
420: || fields.contains("*")) {
421: fields = null; // null means return all stored fields
422: }
423: }
424:
425: int sz = ids.size();
426:
427: writer.write('{');
428: incLevel();
429: writeKey("numFound", false);
430: writeInt(null, ids.matches());
431: writer.write(',');
432: writeKey("start", false);
433: writeInt(null, ids.offset());
434:
435: if (includeScore) {
436: writer.write(',');
437: writeKey("maxScore", false);
438: writeFloat(null, ids.maxScore());
439: }
440: writer.write(',');
441: // indent();
442: writeKey("docs", false);
443: writer.write('[');
444:
445: incLevel();
446: boolean first = true;
447:
448: SolrIndexSearcher searcher = req.getSearcher();
449: DocIterator iterator = ids.iterator();
450: for (int i = 0; i < sz; i++) {
451: int id = iterator.nextDoc();
452: Document doc = searcher.doc(id, fields);
453:
454: if (first) {
455: first = false;
456: } else {
457: writer.write(',');
458: }
459: indent();
460: writeDoc(null, doc, fields, (includeScore ? iterator
461: .score() : 0.0f), includeScore);
462: }
463: decLevel();
464: writer.write(']');
465:
466: if (otherFields != null) {
467: writeMap(null, otherFields, true, false);
468: }
469:
470: decLevel();
471: indent();
472: writer.write('}');
473: }
474:
475: public void writeStr(String name, String val, boolean needsEscaping)
476: throws IOException {
477: writer.write('"');
478: // it might be more efficient to use a stringbuilder or write substrings
479: // if writing chars to the stream is slow.
480: if (needsEscaping) {
481:
482: /* http://www.ietf.org/internet-drafts/draft-crockford-jsonorg-json-04.txt
483: All Unicode characters may be placed within
484: the quotation marks except for the characters which must be
485: escaped: quotation mark, reverse solidus, and the control
486: characters (U+0000 through U+001F).
487: */
488:
489: for (int i = 0; i < val.length(); i++) {
490: char ch = val.charAt(i);
491: switch (ch) {
492: case '"':
493: case '\\':
494: writer.write('\\');
495: writer.write(ch);
496: break;
497: case '\r':
498: writer.write("\\r");
499: break;
500: case '\n':
501: writer.write("\\n");
502: break;
503: case '\t':
504: writer.write("\\t");
505: break;
506: case '\b':
507: writer.write("\\b");
508: break;
509: case '\f':
510: writer.write("\\f");
511: break;
512: // case '/':
513: default: {
514: if (ch <= 0x1F) {
515: unicodeEscape(writer, ch);
516: } else {
517: writer.write(ch);
518: }
519: }
520: }
521: }
522: } else {
523: writer.write(val);
524: }
525: writer.write('"');
526: }
527:
528: public void writeMap(String name, Map val, boolean excludeOuter,
529: boolean isFirstVal) throws IOException {
530: if (!excludeOuter) {
531: writer.write('{');
532: incLevel();
533: isFirstVal = true;
534: }
535:
536: boolean doIndent = excludeOuter || val.size() > 1;
537:
538: for (Map.Entry entry : (Set<Map.Entry>) val.entrySet()) {
539: Object e = entry.getKey();
540: String k = e == null ? "" : e.toString();
541: Object v = entry.getValue();
542:
543: if (isFirstVal) {
544: isFirstVal = false;
545: } else {
546: writer.write(',');
547: }
548:
549: if (doIndent)
550: indent();
551: writeKey(k, true);
552: writeVal(k, v);
553: }
554:
555: if (!excludeOuter) {
556: decLevel();
557: writer.write('}');
558: }
559: }
560:
561: public void writeArray(String name, Object[] val)
562: throws IOException {
563: writeArray(name, Arrays.asList(val).iterator());
564: }
565:
566: public void writeArray(String name, Iterator val)
567: throws IOException {
568: writer.write('[');
569: incLevel();
570: boolean first = true;
571: while (val.hasNext()) {
572: if (!first)
573: indent();
574: writeVal(null, val.next());
575: if (val.hasNext()) {
576: writer.write(',');
577: }
578: first = false;
579: }
580: decLevel();
581: writer.write(']');
582: }
583:
584: //
585: // Primitive types
586: //
587: public void writeNull(String name) throws IOException {
588: writer.write("null");
589: }
590:
591: public void writeInt(String name, String val) throws IOException {
592: writer.write(val);
593: }
594:
595: public void writeLong(String name, String val) throws IOException {
596: writer.write(val);
597: }
598:
599: public void writeBool(String name, String val) throws IOException {
600: writer.write(val);
601: }
602:
603: public void writeFloat(String name, String val) throws IOException {
604: writer.write(val);
605: }
606:
607: public void writeDouble(String name, String val) throws IOException {
608: writer.write(val);
609: }
610:
611: // TODO: refactor this out to a DateUtils class or something...
612: public void writeDate(String name, Date val) throws IOException {
613: // using a stringBuilder for numbers can be nice since
614: // a temporary string isn't used (it's added directly to the
615: // builder's buffer.
616:
617: StringBuilder sb = new StringBuilder();
618: if (cal == null)
619: cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
620: cal.setTime(val);
621:
622: int i = cal.get(Calendar.YEAR);
623: sb.append(i);
624: sb.append('-');
625: i = cal.get(Calendar.MONTH) + 1; // 0 based, so add 1
626: if (i < 10)
627: sb.append('0');
628: sb.append(i);
629: sb.append('-');
630: i = cal.get(Calendar.DAY_OF_MONTH);
631: if (i < 10)
632: sb.append('0');
633: sb.append(i);
634: sb.append('T');
635: i = cal.get(Calendar.HOUR_OF_DAY); // 24 hour time format
636: if (i < 10)
637: sb.append('0');
638: sb.append(i);
639: sb.append(':');
640: i = cal.get(Calendar.MINUTE);
641: if (i < 10)
642: sb.append('0');
643: sb.append(i);
644: sb.append(':');
645: i = cal.get(Calendar.SECOND);
646: if (i < 10)
647: sb.append('0');
648: sb.append(i);
649: i = cal.get(Calendar.MILLISECOND);
650: if (i != 0) {
651: sb.append('.');
652: if (i < 100)
653: sb.append('0');
654: if (i < 10)
655: sb.append('0');
656: sb.append(i);
657:
658: // handle canonical format specifying fractional
659: // seconds shall not end in '0'. Given the slowness of
660: // integer div/mod, simply checking the last character
661: // is probably the fastest way to check.
662: int lastIdx = sb.length() - 1;
663: if (sb.charAt(lastIdx) == '0') {
664: lastIdx--;
665: if (sb.charAt(lastIdx) == '0') {
666: lastIdx--;
667: }
668: sb.setLength(lastIdx + 1);
669: }
670:
671: }
672: sb.append('Z');
673: writeDate(name, sb.toString());
674: }
675:
676: public void writeDate(String name, String val) throws IOException {
677: writeStr(name, val, false);
678: }
679:
680: protected static void unicodeEscape(Appendable sb, int ch)
681: throws IOException {
682: String str = Integer.toHexString(ch & 0xffff);
683: switch (str.length()) {
684: case 1:
685: sb.append("\\u000");
686: break;
687: case 2:
688: sb.append("\\u00");
689: break;
690: case 3:
691: sb.append("\\u0");
692: break;
693: default:
694: sb.append("\\u");
695: break;
696: }
697: sb.append(str);
698: }
699:
700: }
701:
702: class PythonWriter extends JSONWriter {
703: public PythonWriter(Writer writer, SolrQueryRequest req,
704: SolrQueryResponse rsp) {
705: super (writer, req, rsp);
706: }
707:
708: @Override
709: public void writeNull(String name) throws IOException {
710: writer.write("None");
711: }
712:
713: @Override
714: public void writeBool(String name, boolean val) throws IOException {
715: writer.write(val ? "True" : "False");
716: }
717:
718: @Override
719: public void writeBool(String name, String val) throws IOException {
720: writeBool(name, val.charAt(0) == 't');
721: }
722:
723: /* optionally use a unicode python string if necessary */
724: @Override
725: public void writeStr(String name, String val, boolean needsEscaping)
726: throws IOException {
727: if (!needsEscaping) {
728: writer.write('\'');
729: writer.write(val);
730: writer.write('\'');
731: return;
732: }
733:
734: // use python unicode strings...
735: // python doesn't tolerate newlines in strings in it's eval(), so we must escape them.
736:
737: StringBuilder sb = new StringBuilder(val.length());
738: boolean needUnicode = false;
739:
740: for (int i = 0; i < val.length(); i++) {
741: char ch = val.charAt(i);
742: switch (ch) {
743: case '\'':
744: case '\\':
745: sb.append('\\');
746: sb.append(ch);
747: break;
748: case '\r':
749: sb.append("\\r");
750: break;
751: case '\n':
752: sb.append("\\n");
753: break;
754: case '\t':
755: sb.append("\\t");
756: break;
757: default:
758: // we don't strictly have to escape these chars, but it will probably increase
759: // portability to stick to visible ascii
760: if (ch < ' ' || ch > 127) {
761: unicodeEscape(sb, ch);
762: needUnicode = true;
763: } else {
764: sb.append(ch);
765: }
766: }
767: }
768:
769: writer.write(needUnicode ? "u'" : "'");
770: writer.append(sb);
771: writer.write('\'');
772: }
773:
774: /*
775: old version that always used unicode
776: public void writeStr(String name, String val, boolean needsEscaping) throws IOException {
777: // use python unicode strings...
778: // python doesn't tolerate newlines in strings in it's eval(), so we must escape them.
779: writer.write("u'");
780: // it might be more efficient to use a stringbuilder or write substrings
781: // if writing chars to the stream is slow.
782: if (needsEscaping) {
783: for (int i=0; i<val.length(); i++) {
784: char ch = val.charAt(i);
785: switch(ch) {
786: case '\'':
787: case '\\': writer.write('\\'); writer.write(ch); break;
788: case '\r': writer.write("\\r"); break;
789: case '\n': writer.write("\\n"); break;
790: default:
791: // we don't strictly have to escape these chars, but it will probably increase
792: // portability to stick to visible ascii
793: if (ch<' ' || ch>127) {
794: unicodeChar(ch);
795: } else {
796: writer.write(ch);
797: }
798: }
799: }
800: } else {
801: writer.write(val);
802: }
803: writer.write('\'');
804: }
805: */
806:
807: }
808:
809: class RubyWriter extends JSONWriter {
810: public RubyWriter(Writer writer, SolrQueryRequest req,
811: SolrQueryResponse rsp) {
812: super (writer, req, rsp);
813: }
814:
815: @Override
816: public void writeNull(String name) throws IOException {
817: writer.write("nil");
818: }
819:
820: @Override
821: protected void writeKey(String fname, boolean needsEscaping)
822: throws IOException {
823: writeStr(null, fname, needsEscaping);
824: writer.write("=>");
825: }
826:
827: @Override
828: public void writeStr(String name, String val, boolean needsEscaping)
829: throws IOException {
830: // Ruby doesn't do unicode escapes... so let the servlet container write raw UTF-8
831: // bytes into the string.
832: //
833: // Use single quoted strings for safety since no evaluation is done within them.
834: // Also, there are very few escapes recognized in a single quoted string, so
835: // only escape the backslash and single quote.
836: writer.write('\'');
837: // it might be more efficient to use a stringbuilder or write substrings
838: // if writing chars to the stream is slow.
839: if (needsEscaping) {
840: for (int i = 0; i < val.length(); i++) {
841: char ch = val.charAt(i);
842: switch (ch) {
843: case '\'':
844: case '\\':
845: writer.write('\\');
846: writer.write(ch);
847: break;
848: default:
849: writer.write(ch);
850: break;
851: }
852: }
853: } else {
854: writer.write(val);
855: }
856: writer.write('\'');
857: }
858: }
|