You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by no...@apache.org on 2016/09/06 07:53:59 UTC

lucene-solr:branch_6x: SOLR-9127: Excel workbook (.xlsx) response writer. use 'wt=xlsx'

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_6x b32de1ec7 -> 932c436d0


SOLR-9127: Excel workbook (.xlsx) response writer. use 'wt=xlsx'


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

Branch: refs/heads/branch_6x
Commit: 932c436d07dd3fd2cef0f1cb8319d5953433687e
Parents: b32de1e
Author: Noble Paul <no...@apache.org>
Authored: Tue Sep 6 13:22:18 2016 +0530
Committer: Noble Paul <no...@apache.org>
Committed: Tue Sep 6 13:23:29 2016 +0530

----------------------------------------------------------------------
 solr/CHANGES.txt                                |   4 +-
 .../handler/extraction/XLSXResponseWriter.java  | 414 +++++++++++++++++++
 .../extraction/solr/collection1/conf/schema.xml |   2 +
 .../extraction/TestXLSXResponseWriter.java      | 257 ++++++++++++
 .../src/java/org/apache/solr/core/SolrCore.java |   8 +-
 5 files changed, 683 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/932c436d/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 2c77acc..32004ca 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -44,7 +44,9 @@ prefix, then you will now get an error as these options are incompatible with nu
 New Features
 ----------------------
 * SOLR-5725: facet.method=enum can bypass exact counts calculation with facet.exists=true, it just returns 1 for 
-  terms which exists in result docset. (Alexey Kozhemiakin, Sebastian Koziel, Radoslaw Zielinski via Mikhail Khludnev) 
+  terms which exists in result docset. (Alexey Kozhemiakin, Sebastian Koziel, Radoslaw Zielinski via Mikhail Khludnev)
+
+* SOLR-9127: Excel workbook (.xlsx) response writer. use 'wt=xlsx' (Tony Moriarty, noble)
 
 Bug Fixes
 ----------------------

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/932c436d/solr/contrib/extraction/src/java/org/apache/solr/handler/extraction/XLSXResponseWriter.java
----------------------------------------------------------------------
diff --git a/solr/contrib/extraction/src/java/org/apache/solr/handler/extraction/XLSXResponseWriter.java b/solr/contrib/extraction/src/java/org/apache/solr/handler/extraction/XLSXResponseWriter.java
new file mode 100644
index 0000000..27a30d1
--- /dev/null
+++ b/solr/contrib/extraction/src/java/org/apache/solr/handler/extraction/XLSXResponseWriter.java
@@ -0,0 +1,414 @@
+/*
+ * 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.handler.extraction;
+
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import org.apache.lucene.index.IndexableField;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.BasicResultContext;
+import org.apache.solr.response.RawResponseWriter;
+import org.apache.solr.response.ResultContext;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.response.TextResponseWriter;
+import org.apache.solr.schema.FieldType;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.schema.StrField;
+import org.apache.solr.search.DocList;
+import org.apache.solr.search.ReturnFields;
+
+public class XLSXResponseWriter extends RawResponseWriter {
+
+  @Override
+  public void write(OutputStream out, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
+    // throw away arraywriter just to satisfy super requirements; we're grabbing
+    // all writes before they go to it anyway
+    XLSXWriter w = new XLSXWriter(new CharArrayWriter(), req, rsp);
+
+    LinkedHashMap<String,String> reqNamesMap = new LinkedHashMap<>();
+    LinkedHashMap<String,Integer> reqWidthsMap = new LinkedHashMap<>();
+
+    Iterator<String> paramNamesIter = req.getParams().getParameterNamesIterator();
+    while (paramNamesIter.hasNext()) {
+      String nextParam = paramNamesIter.next();
+      if (nextParam.startsWith("colname.")) {
+        String field = nextParam.substring("colname.".length());
+        reqNamesMap.put(field, req.getParams().get(nextParam));
+      } else if (nextParam.startsWith("colwidth.")) {
+        String field = nextParam.substring("colwidth.".length());
+        reqWidthsMap.put(field, req.getParams().getInt(nextParam));
+      }
+    }
+
+    try {
+      w.writeResponse(out, reqNamesMap, reqWidthsMap);
+    } finally {
+      w.close();
+    }
+  }
+
+  @Override
+  public String getContentType(SolrQueryRequest request, SolrQueryResponse response) {
+    return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+  }
+}
+
+class XLSXWriter extends TextResponseWriter {
+
+  SolrQueryRequest req;
+  SolrQueryResponse rsp;
+
+  class SerialWriteWorkbook {
+    SXSSFWorkbook swb;
+    Sheet sh;
+
+    XSSFCellStyle headerStyle;
+    int rowIndex;
+    Row curRow;
+    int cellIndex;
+
+    SerialWriteWorkbook() {
+      this.swb = new SXSSFWorkbook(100);
+      this.sh = this.swb.createSheet();
+
+      this.rowIndex = 0;
+
+      this.headerStyle = (XSSFCellStyle)swb.createCellStyle();
+      this.headerStyle.setFillBackgroundColor(IndexedColors.BLACK.getIndex());
+      //solid fill
+      this.headerStyle.setFillPattern((short)1);
+      Font headerFont = swb.createFont();
+      headerFont.setFontHeightInPoints((short)14);
+      headerFont.setBoldweight(Font.BOLDWEIGHT_BOLD);
+      headerFont.setColor(IndexedColors.WHITE.getIndex());
+      this.headerStyle.setFont(headerFont);
+    }
+
+    void addRow() {
+      curRow = sh.createRow(rowIndex++);
+      cellIndex = 0;
+    }
+
+    void setHeaderRow() {
+      curRow.setHeightInPoints((short)21);
+    }
+
+    //sets last created cell to have header style
+    void setHeaderCell() {
+      curRow.getCell(cellIndex - 1).setCellStyle(this.headerStyle);
+    }
+
+    //set the width of the most recently created column
+    void setColWidth(int charWidth) {
+      //width in poi is units of 1/256th of a character width for some reason
+      this.sh.setColumnWidth(cellIndex - 1, 256*charWidth);
+    }
+
+    void writeCell(String value) {
+      Cell cell = curRow.createCell(cellIndex++);
+      cell.setCellValue(value);
+    }
+
+    void flush(OutputStream out) {
+      try {
+        swb.write(out);
+      } catch (IOException e) {
+        StringWriter sw = new StringWriter();
+        e.printStackTrace(new PrintWriter(sw));
+        String stacktrace = sw.toString();
+      }finally {
+        swb.dispose();
+      }
+    }
+  }
+
+  private SerialWriteWorkbook wb = new SerialWriteWorkbook();
+
+  static class XLField {
+    String name;
+    SchemaField sf;
+  }
+
+  private Map<String,XLField> xlFields = new LinkedHashMap<String,XLField>();
+
+  public XLSXWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp){
+    super(writer, req, rsp);
+    this.req = req;
+    this.rsp = rsp;
+  }
+
+  public void writeResponse(OutputStream out, LinkedHashMap<String, String> colNamesMap,
+                            LinkedHashMap<String, Integer> colWidthsMap) throws IOException {
+    SolrParams params = req.getParams();
+
+    Collection<String> fields = returnFields.getRequestedFieldNames();
+    Object responseObj = rsp.getValues().get("response");
+    boolean returnOnlyStored = false;
+    if (fields==null||returnFields.hasPatternMatching()) {
+      if (responseObj instanceof SolrDocumentList) {
+        // get the list of fields from the SolrDocumentList
+        if(fields==null) {
+          fields = new LinkedHashSet<String>();
+        }
+        for (SolrDocument sdoc: (SolrDocumentList)responseObj) {
+          fields.addAll(sdoc.getFieldNames());
+        }
+      } else {
+        // get the list of fields from the index
+        Iterable<String> all = req.getSearcher().getFieldNames();
+        if (fields == null) {
+          fields = Sets.newHashSet(all);
+        } else {
+          Iterables.addAll(fields, all);
+        }
+      }
+      if (returnFields.wantsScore()) {
+        fields.add("score");
+      } else {
+        fields.remove("score");
+      }
+      returnOnlyStored = true;
+    }
+
+    for (String field : fields) {
+      if (!returnFields.wantsField(field)) {
+        continue;
+      }
+      if (field.equals("score")) {
+        XLField xlField = new XLField();
+        xlField.name = "score";
+        xlFields.put("score", xlField);
+        continue;
+      }
+
+      SchemaField sf = schema.getFieldOrNull(field);
+      if (sf == null) {
+        FieldType ft = new StrField();
+        sf = new SchemaField(field, ft);
+      }
+
+      // Return only stored fields, unless an explicit field list is specified
+      if (returnOnlyStored && sf != null && !sf.stored()) {
+        continue;
+      }
+
+      XLField xlField = new XLField();
+      xlField.name = field;
+      xlField.sf = sf;
+      xlFields.put(field, xlField);
+    }
+
+
+
+    wb.addRow();
+    //write header
+    for (XLField xlField : xlFields.values()) {
+      String printName = xlField.name;
+      int colWidth = 14;
+
+      String niceName = colNamesMap.get(xlField.name);
+      if (niceName != null) {
+        printName = niceName;
+      }
+
+      Integer niceWidth = colWidthsMap.get(xlField.name);
+      if (niceWidth != null) {
+        colWidth = niceWidth.intValue();
+      }
+
+      writeStr(xlField.name, printName, false);
+      wb.setColWidth(colWidth);
+      wb.setHeaderCell();
+    }
+    wb.setHeaderRow();
+    wb.addRow();
+
+    if (responseObj instanceof ResultContext) {
+      writeDocuments(null, (ResultContext)responseObj );
+    }
+    else if (responseObj instanceof DocList) {
+      ResultContext ctx = new BasicResultContext((DocList)responseObj, returnFields, null, null, req);
+      writeDocuments(null, ctx );
+    } else if (responseObj instanceof SolrDocumentList) {
+      writeSolrDocumentList(null, (SolrDocumentList)responseObj, returnFields );
+    }
+
+    wb.flush(out);
+    wb = null;
+  }
+
+  @Override
+  public void close() throws IOException {
+    super.close();
+  }
+
+  @Override
+  public void writeNamedList(String name, NamedList val) throws IOException {
+  }
+
+  @Override
+  public void writeStartDocumentList(String name,
+                                     long start, int size, long numFound, Float maxScore) throws IOException
+  {
+    // nothing
+  }
+
+  @Override
+  public void writeEndDocumentList() throws IOException
+  {
+    // nothing
+  }
+
+  //NOTE: a document cannot currently contain another document
+  List tmpList;
+  @Override
+  public void writeSolrDocument(String name, SolrDocument doc, ReturnFields returnFields, int idx ) throws IOException {
+    if (tmpList == null) {
+      tmpList = new ArrayList(1);
+      tmpList.add(null);
+    }
+
+    for (XLField xlField : xlFields.values()) {
+      Object val = doc.getFieldValue(xlField.name);
+      int nVals = val instanceof Collection ? ((Collection)val).size() : (val==null ? 0 : 1);
+      if (nVals == 0) {
+        writeNull(xlField.name);
+        continue;
+      }
+
+      if ((xlField.sf != null && xlField.sf.multiValued()) || nVals > 1) {
+        Collection values;
+        // normalize to a collection
+        if (val instanceof Collection) {
+          values = (Collection)val;
+        } else {
+          tmpList.set(0, val);
+          values = tmpList;
+        }
+
+        writeArray(xlField.name, values.iterator());
+
+      } else {
+        // normalize to first value
+        if (val instanceof Collection) {
+          Collection values = (Collection)val;
+          val = values.iterator().next();
+        }
+        writeVal(xlField.name, val);
+      }
+    }
+    wb.addRow();
+  }
+
+  @Override
+  public void writeStr(String name, String val, boolean needsEscaping) throws IOException {
+    wb.writeCell(val);
+  }
+
+  @Override
+  public void writeMap(String name, Map val, boolean excludeOuter, boolean isFirstVal) throws IOException {
+  }
+
+  @Override
+  public void writeArray(String name, Iterator val) throws IOException {
+    StringBuffer output = new StringBuffer();
+    while (val.hasNext()) {
+      Object v = val.next();
+      if (v instanceof IndexableField) {
+        IndexableField f = (IndexableField)v;
+        if (v instanceof Date) {
+          output.append(((Date) val).toInstant().toString() + "; ");
+        } else {
+          output.append(f.stringValue() + "; ");
+        }
+      } else {
+        output.append(v.toString() + "; ");
+      }
+    }
+    if (output.length() > 0) {
+      output.deleteCharAt(output.length()-1);
+      output.deleteCharAt(output.length()-1);
+    }
+    writeStr(name, output.toString(), false);
+  }
+
+  @Override
+  public void writeNull(String name) throws IOException {
+    wb.writeCell("");
+  }
+
+  @Override
+  public void writeInt(String name, String val) throws IOException {
+    wb.writeCell(val);
+  }
+
+  @Override
+  public void writeLong(String name, String val) throws IOException {
+    wb.writeCell(val);
+  }
+
+  @Override
+  public void writeBool(String name, String val) throws IOException {
+    wb.writeCell(val);
+  }
+
+  @Override
+  public void writeFloat(String name, String val) throws IOException {
+    wb.writeCell(val);
+  }
+
+  @Override
+  public void writeDouble(String name, String val) throws IOException {
+    wb.writeCell(val);
+  }
+
+  @Override
+  public void writeDate(String name, Date val) throws IOException {
+    writeDate(name, val.toInstant().toString());
+  }
+
+  @Override
+  public void writeDate(String name, String val) throws IOException {
+    wb.writeCell(val);
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/932c436d/solr/contrib/extraction/src/test-files/extraction/solr/collection1/conf/schema.xml
----------------------------------------------------------------------
diff --git a/solr/contrib/extraction/src/test-files/extraction/solr/collection1/conf/schema.xml b/solr/contrib/extraction/src/test-files/extraction/solr/collection1/conf/schema.xml
index 35d123f..bd9adbe 100644
--- a/solr/contrib/extraction/src/test-files/extraction/solr/collection1/conf/schema.xml
+++ b/solr/contrib/extraction/src/test-files/extraction/solr/collection1/conf/schema.xml
@@ -415,6 +415,7 @@
   -->
   <dynamicField name="*_i" type="int" indexed="true" stored="true"/>
   <dynamicField name="*_s" type="string" indexed="true" stored="true"/>
+  <dynamicField name="*_ss" type="string" indexed="true" stored="true" multiValued="true"/>
   <dynamicField name="*_s1" type="string" indexed="true" stored="true" multiValued="false"/>
   <dynamicField name="*_l" type="long" indexed="true" stored="true"/>
   <dynamicField name="*_t" type="text" indexed="true" stored="true"/>
@@ -422,6 +423,7 @@
   <dynamicField name="*_f" type="float" indexed="true" stored="true"/>
   <dynamicField name="*_d" type="double" indexed="true" stored="true"/>
   <dynamicField name="*_dt" type="date" indexed="true" stored="true"/>
+  <dynamicField name="*_dt1" type="date" indexed="true" stored="true" multiValued="false"/>
 
   <dynamicField name="*_sI" type="string" indexed="true" stored="false"/>
   <dynamicField name="*_sS" type="string" indexed="false" stored="true"/>

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/932c436d/solr/contrib/extraction/src/test/org/apache/solr/handler/extraction/TestXLSXResponseWriter.java
----------------------------------------------------------------------
diff --git a/solr/contrib/extraction/src/test/org/apache/solr/handler/extraction/TestXLSXResponseWriter.java b/solr/contrib/extraction/src/test/org/apache/solr/handler/extraction/TestXLSXResponseWriter.java
new file mode 100644
index 0000000..fd4e63d
--- /dev/null
+++ b/solr/contrib/extraction/src/test/org/apache/solr/handler/extraction/TestXLSXResponseWriter.java
@@ -0,0 +1,257 @@
+/*
+ * 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.handler.extraction;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Date;
+
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.response.RawResponseWriter;
+import org.apache.solr.search.SolrReturnFields;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestXLSXResponseWriter extends SolrTestCaseJ4 {
+
+  private static XLSXResponseWriter writerXlsx;
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    System.setProperty("enable.update.log", "false");
+    initCore("solrconfig.xml","schema.xml",getFile("extraction/solr").getAbsolutePath());
+    createIndex();
+    //find a reference to the default response writer so we can redirect its output later
+    SolrCore testCore = h.getCore();
+    QueryResponseWriter writer = testCore.getQueryResponseWriter("xlsx");
+    if (writer instanceof XLSXResponseWriter) {
+      writerXlsx = (XLSXResponseWriter) testCore.getQueryResponseWriter("xlsx");
+    } else {
+      throw new Exception("XLSXResponseWriter not registered with solr core");
+    }
+  }
+
+  public static void createIndex() {
+    assertU(adoc("id","1", "foo_i","-1", "foo_s","hi", "foo_l","12345678987654321", "foo_b","false", "foo_f","1.414","foo_d","-1.0E300","foo_dt1","2000-01-02T03:04:05Z"));
+    assertU(adoc("id","2", "v_ss","hi",  "v_ss","there", "v2_ss","nice", "v2_ss","output", "shouldbeunstored","foo"));
+    assertU(adoc("id","3", "shouldbeunstored","foo"));
+    assertU(adoc("id","4", "foo_s1","foo"));
+    assertU(commit());
+  }
+
+  @AfterClass
+  public static void cleanupWriter() throws Exception {
+    writerXlsx = null;
+  }
+
+  @Test
+  public void testStructuredDataViaBaseWriters() throws IOException, Exception {
+    SolrQueryResponse rsp = new SolrQueryResponse();
+    // Don't send a ContentStream back, this will fall back to the configured base writer.
+    // But abuse the CONTENT key to ensure writer is also checking type
+    rsp.add(RawResponseWriter.CONTENT, "test");
+    rsp.add("foo", "bar");
+
+    SolrQueryRequest r = req();
+
+    // check Content-Type
+    assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", writerXlsx.getContentType(r, rsp));
+
+    // test our basic types,and that fields come back in the requested order
+    XSSFSheet resultSheet = getWSResultForQuery(req("q","id:1", "wt","xlsx", "fl","id,foo_s,foo_i,foo_l,foo_b,foo_f,foo_d,foo_dt1"));
+
+    assertEquals("id,foo_s,foo_i,foo_l,foo_b,foo_f,foo_d,foo_dt1\n1,hi,-1,12345678987654321,F,1.414,-1.0E300,2000-01-02T03:04:05Z\n"
+        , getStringFromSheet(resultSheet));
+
+    resultSheet = getWSResultForQuery(req("q","id:1^0", "wt","xlsx", "fl","id,score,foo_s"));
+    // test retrieving score
+    assertEquals("id,score,foo_s\n1,0.0,hi\n", getStringFromSheet(resultSheet));
+
+    resultSheet = getWSResultForQuery(req("q","id:1^0", "wt","xlsx", "colname.id", "I.D.", "colwidth.id", "10",
+                                      "fl","id,score,foo_s"));
+    // test override colname/width
+    assertEquals("I.D.,score,foo_s\n1,0.0,hi\n", getStringFromSheet(resultSheet));
+    // test colwidth (value returned is in 256ths of a character as per excel standard)
+    assertEquals(10*256, resultSheet.getColumnWidth(0));
+
+    resultSheet = getWSResultForQuery(req("q","id:2", "wt","xlsx", "fl","id,v_ss"));
+    // test multivalued
+    assertEquals("id,v_ss\n2,hi; there\n", getStringFromSheet(resultSheet));
+
+    // test retrieving fields from index
+    resultSheet = getWSResultForQuery(req("q","*:*", "wt","xslx", "fl","*,score"));
+    String result = getStringFromSheet(resultSheet);
+    for (String field : "id,foo_s,foo_i,foo_l,foo_b,foo_f,foo_d,foo_dt1,v_ss,v2_ss,score".split(",")) {
+      assertTrue(result.indexOf(field) >= 0);
+    }
+
+    // test null values
+    resultSheet = getWSResultForQuery(req("q","id:2", "wt","xlsx", "fl","id,foo_s,v_ss"));
+    assertEquals("id,foo_s,v_ss\n2,,hi; there\n", getStringFromSheet(resultSheet));
+
+    // now test SolrDocumentList
+    SolrDocument d = new SolrDocument();
+    SolrDocument d1 = d;
+    d.addField("id","1");
+    d.addField("foo_i",-1);
+    d.addField("foo_s","hi");
+    d.addField("foo_l","12345678987654321L");
+    d.addField("foo_b",false);
+    d.addField("foo_f",1.414f);
+    d.addField("foo_d",-1.0E300);
+    d.addField("foo_dt1", new Date(Instant.parse("2000-01-02T03:04:05Z").toEpochMilli()));
+    d.addField("score", "2.718");
+
+    d = new SolrDocument();
+    SolrDocument d2 = d;
+    d.addField("id","2");
+    d.addField("v_ss","hi");
+    d.addField("v_ss","there");
+    d.addField("v2_ss","nice");
+    d.addField("v2_ss","output");
+    d.addField("score", "89.83");
+    d.addField("shouldbeunstored","foo");
+
+    SolrDocumentList sdl = new SolrDocumentList();
+    sdl.add(d1);
+    sdl.add(d2);
+    
+    SolrQueryRequest req = req("q","*:*");
+    rsp = new SolrQueryResponse();
+    rsp.addResponse(sdl);
+
+    rsp.setReturnFields( new SolrReturnFields("id,foo_s", req) );
+
+    resultSheet = getWSResultForQuery(req, rsp);
+    assertEquals("id,foo_s\n1,hi\n2,\n", getStringFromSheet(resultSheet));
+
+    // try scores
+    rsp.setReturnFields( new SolrReturnFields("id,score,foo_s", req) );
+
+    resultSheet = getWSResultForQuery(req, rsp);
+    assertEquals("id,score,foo_s\n1,2.718,hi\n2,89.83,\n", getStringFromSheet(resultSheet));
+
+    // get field values from docs... should be ordered and not include score unless requested
+    rsp.setReturnFields( new SolrReturnFields("*", req) );
+
+    resultSheet = getWSResultForQuery(req, rsp);
+    assertEquals("id,foo_i,foo_s,foo_l,foo_b,foo_f,foo_d,foo_dt1,v_ss,v2_ss\n" +
+        "1,-1,hi,12345678987654321L,false,1.414,-1.0E300,2000-01-02T03:04:05Z,,\n" +
+        "2,,,,,,,,hi; there,nice; output\n", getStringFromSheet(resultSheet));
+
+    // get field values and scores - just check that the scores are there... we don't guarantee where
+    rsp.setReturnFields( new SolrReturnFields("*,score", req) );
+    resultSheet = getWSResultForQuery(req, rsp);
+    String s = getStringFromSheet(resultSheet);
+    assertTrue(s.indexOf("score") >=0 && s.indexOf("2.718") > 0 && s.indexOf("89.83") > 0 );
+    
+    // Test field globs
+    rsp.setReturnFields( new SolrReturnFields("id,foo*", req) );
+    resultSheet = getWSResultForQuery(req, rsp);
+    assertEquals("id,foo_i,foo_s,foo_l,foo_b,foo_f,foo_d,foo_dt1\n" +
+        "1,-1,hi,12345678987654321L,false,1.414,-1.0E300,2000-01-02T03:04:05Z\n" +
+        "2,,,,,,,\n", getStringFromSheet(resultSheet));
+
+    rsp.setReturnFields( new SolrReturnFields("id,*_d*", req) );
+    resultSheet = getWSResultForQuery(req, rsp);
+    assertEquals("id,foo_d,foo_dt1\n" +
+        "1,-1.0E300,2000-01-02T03:04:05Z\n" +
+        "2,,\n", getStringFromSheet(resultSheet));
+
+    // Test function queries
+    rsp.setReturnFields( new SolrReturnFields("sum(1,1),id,exists(foo_s1),div(9,1),foo_f", req) );
+    resultSheet = getWSResultForQuery(req, rsp);
+    assertEquals("sum(1,1),id,exists(foo_s1),div(9,1),foo_f\n" +
+        ",1,,,1.414\n" +
+        ",2,,,\n", getStringFromSheet(resultSheet));
+
+    // Test transformers
+    rsp.setReturnFields( new SolrReturnFields("mydocid:[docid],[explain]", req) );
+    resultSheet = getWSResultForQuery(req, rsp);
+    assertEquals("mydocid,[explain]\n" +
+        ",\n" +
+        ",\n", getStringFromSheet(resultSheet));
+
+    req.close();
+  }
+  
+
+  @Test
+  public void testPseudoFields() throws Exception {
+    // Use Pseudo Field
+    SolrQueryRequest req = req("q","id:1", "wt","xlsx", "fl","XXX:id,foo_s");
+    XSSFSheet resultSheet = getWSResultForQuery(req);
+    assertEquals("XXX,foo_s\n1,hi\n", getStringFromSheet(resultSheet));
+    
+    String txt = getStringFromSheet(getWSResultForQuery(req("q","id:1", "wt","xlsx", "fl","XXX:id,YYY:[docid],FOO:foo_s")));
+    String[] lines = txt.split("\n");
+    assertEquals(2, lines.length);
+    assertEquals("XXX,YYY,FOO", lines[0] );
+    assertEquals("1,0,hi", lines[1] );
+
+    //assertions specific to multiple pseudofields functions like abs, div, exists, etc.. (SOLR-5423)
+    String funcText = getStringFromSheet(getWSResultForQuery(req("q","*", "wt","xlsx", "fl","XXX:id,YYY:exists(foo_s1)")));
+    String[] funcLines = funcText.split("\n");
+    assertEquals(5, funcLines.length);
+    assertEquals("XXX,YYY", funcLines[0] );
+    assertEquals("1,false", funcLines[1] );
+    assertEquals("3,false", funcLines[3] );
+  }
+
+  // returns first worksheet as XLSXResponseWriter only returns one sheet
+  private XSSFSheet getWSResultForQuery(SolrQueryRequest req) throws IOException, Exception {
+    SolrQueryResponse rsp = h.queryAndResponse("standard", req);
+    return getWSResultForQuery(req, rsp);
+  }
+
+  private XSSFSheet getWSResultForQuery(SolrQueryRequest req, SolrQueryResponse rsp) throws IOException, Exception {
+    ByteArrayOutputStream xmlBout = new ByteArrayOutputStream();
+    writerXlsx.write(xmlBout, req, rsp);
+    XSSFWorkbook output = new XSSFWorkbook(new ByteArrayInputStream(xmlBout.toByteArray()));
+    XSSFSheet sheet = output.getSheetAt(0);
+    req.close();
+    output.close();
+    return sheet;
+  }
+
+  private String getStringFromSheet(XSSFSheet sheet) {
+    StringBuilder output = new StringBuilder();
+    for (Row row: sheet) {
+      for (Cell cell: row) {
+        output.append(cell.getStringCellValue());
+        output.append(",");
+      }
+      output.setLength(output.length() - 1);
+      output.append("\n");
+    }
+    return output.toString();
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/932c436d/solr/core/src/java/org/apache/solr/core/SolrCore.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java b/solr/core/src/java/org/apache/solr/core/SolrCore.java
index 2704e4a..ac966f5 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -2197,6 +2197,12 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
     m.put("smile", new SmileResponseWriter());
     m.put(ReplicationHandler.FILE_STREAM, getFileStreamWriter());
     DEFAULT_RESPONSE_WRITERS = Collections.unmodifiableMap(m);
+    try {
+      m.put("xlsx",
+          (QueryResponseWriter) Class.forName("org.apache.solr.handler.extraction.XLSXResponseWriter").newInstance());
+    } catch (Exception e) {
+      //don't worry; solrcell contrib not in class path
+    }
   }
 
   private static BinaryResponseWriter getFileStreamWriter() {
@@ -2219,7 +2225,7 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
   }
 
   public interface RawWriter {
-    public void write(OutputStream os) throws IOException ;
+    void write(OutputStream os) throws IOException ;
   }
 
   /** Configure the query response writers. There will always be a default writer; additional