diff --git a/src/metabase/query_processor/streaming/xlsx.clj b/src/metabase/query_processor/streaming/xlsx.clj index 1ca44891739b548814d23f5213b5369722914579..7b6fc7d98840723b825e091ff92715f7a1e668e6 100644 --- a/src/metabase/query_processor/streaming/xlsx.clj +++ b/src/metabase/query_processor/streaming/xlsx.clj @@ -2,14 +2,12 @@ (:require [cheshire.core :as json] [dk.ative.docjure.spreadsheet :as spreadsheet] [java-time :as t] - [metabase.query-processor.streaming - [common :as common] - [interface :as i]] + [metabase.query-processor.streaming.interface :as i] [metabase.util [date-2 :as u.date] [i18n :refer [tru]]]) (:import java.io.OutputStream - org.apache.poi.ss.usermodel.Cell + [org.apache.poi.ss.usermodel Cell CellType] org.apache.poi.xssf.streaming.SXSSFWorkbook)) (defmethod i/stream-options :xlsx @@ -19,12 +17,45 @@ :headers {"Content-Disposition" (format "attachment; filename=\"query_result_%s.xlsx\"" (u.date/format (t/zoned-date-time)))}}) +(defprotocol ExcelFormatValue + "Protocol for specifying how objects of various classes in QP result rows should be formatted for Excel + exports. The generic version in `common/FormatValue` does not work for Excel due to date/time formats." + (excel-format-value [this] + "Format this value in a QP result row appropriately for a results download to Excel.")) + +;; this version of the protocol resolves #10803 by not converting datetimes to ISO8601 (which Excel cannot parse) +;; Docjure can handle java.util.Date instances, but not the new Java 8 Time instances, so we convert everything +;; to java.util.Date. +(extend-protocol ExcelFormatValue + nil + (excel-format-value [_] nil) + + Object + (excel-format-value [this] this) + + java.time.temporal.Temporal + (excel-format-value [this] + (t/java-date this)) + + java.time.LocalDateTime + (excel-format-value [this] + ;; this is a sticky one - LocalDateTime where? Using the server's timezone for the conversion. + (t/java-date (t/zoned-date-time this (t/zone-id)))) + + java.time.OffsetDateTime + (excel-format-value [this] + (t/java-date this)) + + java.time.ZonedDateTime + (excel-format-value [this] + (t/java-date this))) + ;; add a generic implementation for the method that writes values to XLSX cells that just piggybacks off the ;; implementations we've already defined for encoding things as JSON. These implementations live in ;; `metabase.middleware`. (defmethod spreadsheet/set-cell! Object [^Cell cell, value] - (when (= (.getCellType cell) Cell/CELL_TYPE_FORMULA) - (.setCellType cell Cell/CELL_TYPE_STRING)) + (when (= (.getCellType cell) CellType/FORMULA) + (.setCellType cell CellType/STRING)) ;; stick the object in a JSON map and encode it, which will force conversion to a string. Then unparse that JSON and ;; use the resulting value as the cell's new String value. There might be some more efficient way of doing this but ;; I'm not sure what it is. @@ -42,7 +73,7 @@ (spreadsheet/add-row! sheet (map (some-fn :display_name :name) cols))) (write-row! [_ row _] - (spreadsheet/add-row! sheet (map common/format-value row))) + (spreadsheet/add-row! sheet (map excel-format-value row))) (finish! [_ _] (spreadsheet/save-workbook-into-stream! os workbook)