diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStyler.java
similarity index 85%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStyler.java
index 12dc6685..45d37640 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStyler.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2022. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
import org.dhatim.fastexcel.BorderSide;
@@ -30,7 +30,7 @@ import org.dhatim.fastexcel.StyleSetter;
/*******************************************************************************
** Version of excel styler that does bold headers and footers, with basic borders.
*******************************************************************************/
-public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
+public class BoldHeaderAndFooterFastExcelStyler implements FastExcelStylerInterface
{
/*******************************************************************************
@@ -60,6 +60,9 @@ public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
+ /*******************************************************************************
+ **
+ *******************************************************************************/
@Override
public void styleTotalsRow(StyleSetter totalsRowStyle)
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/ExcelFastexcelExportStreamer.java
similarity index 89%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/ExcelFastexcelExportStreamer.java
index 61f14588..fd1dc915 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/ExcelFastexcelExportStreamer.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2022. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.actions.reporting;
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
import java.io.OutputStream;
@@ -34,8 +34,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.ExcelStylerInterface;
-import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler;
+import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
+import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
@@ -43,7 +43,9 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.dhatim.fastexcel.StyleSetter;
@@ -52,19 +54,19 @@ import org.dhatim.fastexcel.Worksheet;
/*******************************************************************************
- ** Excel export format implementation
+ ** Excel export format implementation - built using fastexcel library
*******************************************************************************/
-public class ExcelExportStreamer implements ExportStreamerInterface
+public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
{
- private static final QLogger LOG = QLogger.getLogger(ExcelExportStreamer.class);
+ private static final QLogger LOG = QLogger.getLogger(ExcelFastexcelExportStreamer.class);
private ExportInput exportInput;
private QTableMetaData table;
private List fields;
private OutputStream outputStream;
- private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
- private Map excelCellFormats;
+ private FastExcelStylerInterface fastExcelStylerInterface = new PlainFastExcelStyler();
+ private Map excelCellFormats;
private Workbook workbook;
private Worksheet worksheet;
@@ -76,7 +78,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
/*******************************************************************************
**
*******************************************************************************/
- public ExcelExportStreamer()
+ public ExcelFastexcelExportStreamer()
{
}
@@ -105,14 +107,14 @@ public class ExcelExportStreamer implements ExportStreamerInterface
** Starts a new worksheet in the current workbook. Can be called multiple times.
*******************************************************************************/
@Override
- public void start(ExportInput exportInput, List fields, String label) throws QReportingException
+ public void start(ExportInput exportInput, List fields, String label, QReportView view) throws QReportingException
{
try
{
this.exportInput = exportInput;
this.fields = fields;
table = exportInput.getTable();
- outputStream = this.exportInput.getReportOutputStream();
+ outputStream = this.exportInput.getReportDestination().getReportOutputStream();
this.row = 0;
this.sheetCount++;
@@ -121,7 +123,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
/////////////////////////////////////////////////////////////////////////////////////////////////////
if(workbook == null)
{
- String appName = "QQQ";
+ String appName = ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getBranding().getAppName(), "QQQ");
QInstance instance = exportInput.getInstance();
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
{
@@ -167,7 +169,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
worksheet.range(row, 0, row, fields.size() - 1).merge();
StyleSetter titleStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
- excelStylerInterface.styleTitleRow(titleStyle);
+ fastExcelStylerInterface.styleTitleRow(titleStyle);
titleStyle.set();
row++;
@@ -187,7 +189,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
}
StyleSetter headerStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
- excelStylerInterface.styleHeaderRow(headerStyle);
+ fastExcelStylerInterface.styleHeaderRow(headerStyle);
headerStyle.set();
row++;
@@ -315,7 +317,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
writeRecord(record);
StyleSetter totalsRowStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
- excelStylerInterface.styleTotalsRow(totalsRowStyle);
+ fastExcelStylerInterface.styleTotalsRow(totalsRowStyle);
totalsRowStyle.set();
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/FastExcelStylerInterface.java
similarity index 92%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/FastExcelStylerInterface.java
index ed58aba3..68b361eb 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/FastExcelStylerInterface.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2022. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
import org.dhatim.fastexcel.StyleSetter;
@@ -29,7 +29,7 @@ import org.dhatim.fastexcel.StyleSetter;
** Interface for classes that know how to apply styles to an Excel stream being
** built by fastexcel.
*******************************************************************************/
-public interface ExcelStylerInterface
+public interface FastExcelStylerInterface
{
/*******************************************************************************
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/PlainFastExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/PlainFastExcelStyler.java
new file mode 100644
index 00000000..21254a3a
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/PlainFastExcelStyler.java
@@ -0,0 +1,31 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
+
+
+/*******************************************************************************
+ ** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
+ *******************************************************************************/
+public class PlainFastExcelStyler implements FastExcelStylerInterface
+{
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/BoldHeaderAndFooterPoiExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/BoldHeaderAndFooterPoiExcelStyler.java
new file mode 100644
index 00000000..c30beef1
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/BoldHeaderAndFooterPoiExcelStyler.java
@@ -0,0 +1,93 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
+
+
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+
+/*******************************************************************************
+ ** Version of POI excel styler that does bold headers and footers, with basic borders.
+ *******************************************************************************/
+public class BoldHeaderAndFooterPoiExcelStyler implements PoiExcelStylerInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public XSSFCellStyle createStyleForTitle(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ Font font = workbook.createFont();
+ font.setFontHeightInPoints((short) 14);
+ font.setBold(true);
+
+ XSSFCellStyle cellStyle = workbook.createCellStyle();
+ cellStyle.setFont(font);
+ cellStyle.setAlignment(HorizontalAlignment.CENTER);
+
+ return (cellStyle);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ Font font = workbook.createFont();
+ font.setBold(true);
+
+ XSSFCellStyle cellStyle = workbook.createCellStyle();
+ cellStyle.setFont(font);
+ cellStyle.setBorderBottom(BorderStyle.THIN);
+
+ return (cellStyle);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public XSSFCellStyle createStyleForFooter(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ Font font = workbook.createFont();
+ font.setBold(true);
+
+ XSSFCellStyle cellStyle = workbook.createCellStyle();
+ cellStyle.setFont(font);
+ cellStyle.setBorderTop(BorderStyle.THIN);
+ cellStyle.setBorderBottom(BorderStyle.DOUBLE);
+
+ return (cellStyle);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingExportStreamer.java
new file mode 100644
index 00000000..974dd56b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingExportStreamer.java
@@ -0,0 +1,751 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
+
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Serializable;
+import java.io.Writer;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
+import com.kingsrook.qqq.backend.core.actions.reporting.ReportUtils;
+import com.kingsrook.qqq.backend.core.actions.reporting.pivottable.PivotTableGroupBy;
+import com.kingsrook.qqq.backend.core.actions.reporting.pivottable.PivotTableValue;
+import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
+import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import org.apache.poi.ss.SpreadsheetVersion;
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.ss.usermodel.DataConsolidateFunction;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.util.AreaReference;
+import org.apache.poi.ss.util.CellReference;
+import org.apache.poi.xssf.usermodel.XSSFCell;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFPivotTable;
+import org.apache.poi.xssf.usermodel.XSSFRow;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+
+/*******************************************************************************
+ ** Excel export format implementation using POI library, but with modifications
+ ** to actually stream output rather than use any temp files.
+ *******************************************************************************/
+public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInterface
+{
+ private static final QLogger LOG = QLogger.getLogger(ExcelPoiBasedStreamingExportStreamer.class);
+
+ private List views;
+ private ExportInput exportInput;
+ private List fields;
+ private OutputStream outputStream;
+ private ZipOutputStream zipOutputStream;
+
+ private PoiExcelStylerInterface poiExcelStylerInterface = new PlainPoiExcelStyler();
+ private Map excelCellFormats;
+
+ private int rowNo = 0;
+ private int sheetIndex = 1;
+
+ private Map pivotViewToCacheDefinitionReferenceMap = new HashMap<>();
+ private Map styles = new HashMap<>();
+
+ private Writer activeSheetWriter = null;
+ private StreamedPoiSheetWriter sheetWriter = null;
+
+ private QReportView currentView = null;
+ private Map> fieldsPerView = new HashMap<>();
+ private Map rowsPerView = new HashMap<>();
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public ExcelPoiBasedStreamingExportStreamer()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void preRun(ReportDestination reportDestination, List views) throws QReportingException
+ {
+ try
+ {
+ this.outputStream = reportDestination.getReportOutputStream();
+ this.views = views;
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // create 'template' workbook through poi - with sheets corresponding to our //
+ // actual file this will be a zip file (stream), with entries for all of the //
+ // files in the final xlsx but without any data, so it'll be small //
+ ///////////////////////////////////////////////////////////////////////////////
+ XSSFWorkbook workbook = new XSSFWorkbook();
+ createStyles(workbook);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // for each of the sheets, create it in the workbook, and put a reference to it in the sheetMap //
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ Map sheetMapByExcelReference = new HashMap<>();
+ Map sheetMapByViewName = new HashMap<>();
+
+ int sheetCounter = 1;
+ for(QReportView view : views)
+ {
+ String label = Objects.requireNonNullElse(view.getLabel(), "Sheet " + sheetCounter);
+ XSSFSheet sheet = workbook.createSheet(label);
+ String sheetReference = sheet.getPackagePart().getPartName().getName().substring(1);
+ sheetMapByExcelReference.put(sheetReference, sheet);
+ sheetMapByViewName.put(view.getName(), sheet);
+ sheetCounter++;
+ }
+
+ ////////////////////////////////////////////////////
+ // if any views are pivot tables, create them now //
+ ////////////////////////////////////////////////////
+ List pivotViewNames = new ArrayList<>();
+ for(QReportView view : views)
+ {
+ if(ReportType.PIVOT.equals(view.getType()))
+ {
+ pivotViewNames.add(view.getName());
+
+ XSSFSheet pivotTableSheet = Objects.requireNonNull(sheetMapByViewName.get(view.getName()), "Could not get pivot table sheet view by name: " + view.getName());
+ XSSFSheet dataSheet = Objects.requireNonNull(sheetMapByViewName.get(view.getPivotTableSourceViewName()), "Could not get pivot table source sheet by view name: " + view.getPivotTableSourceViewName());
+ QReportView dataView = ReportUtils.getSourceViewForPivotTableView(views, view);
+ createPivotTableTemplate(pivotTableSheet, view, dataSheet, dataView);
+ }
+ }
+ Iterator pivotViewNameIterator = pivotViewNames.iterator();
+
+ /////////////////////////////////////////////////////////
+ // write that template worksheet zip out to byte array //
+ /////////////////////////////////////////////////////////
+ ByteArrayOutputStream templateBAOS = new ByteArrayOutputStream();
+ workbook.write(templateBAOS);
+ templateBAOS.close();
+ byte[] templateBytes = templateBAOS.toByteArray();
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // open up a zipOutputStream around the output stream that the report is to be written to. //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ this.zipOutputStream = new ZipOutputStream(this.outputStream);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // copy over all the entries in the template zip that aren't the sheets into the output stream //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(templateBytes));
+ ZipEntry zipTemplateEntry = null;
+ byte[] buffer = new byte[2048];
+ while((zipTemplateEntry = zipInputStream.getNextEntry()) != null)
+ {
+ if(zipTemplateEntry.getName().matches(".*/pivotCacheDefinition.*.xml"))
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if this zip entry is a pivotCacheDefinition, then don't write it to the output stream right now. //
+ // instead, just map the pivot view's name to the zipTemplateEntry name //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(!pivotViewNameIterator.hasNext())
+ {
+ throw new QReportingException("Found a pivot cache definition [" + zipTemplateEntry.getName() + "] in the template ZIP, but no (more) corresponding pivot view names");
+ }
+
+ String pivotViewName = pivotViewNameIterator.next();
+ LOG.info("Holding on a pivot cache definition zip template entry [" + pivotViewName + "] [" + zipTemplateEntry.getName() + "]...");
+ pivotViewToCacheDefinitionReferenceMap.put(pivotViewName, zipTemplateEntry.getName());
+ }
+ else if(!sheetMapByExcelReference.containsKey(zipTemplateEntry.getName()))
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // else, if we don't have this zipTemplateEntry name in our map of sheets, then this is a kinda "meta" //
+ // file that we don't really care about (e.g., not our sheet data), so just copy it to the output stream. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ LOG.info("Copying zip template entry [" + zipTemplateEntry.getName() + "] to output stream");
+ zipOutputStream.putNextEntry(new ZipEntry(zipTemplateEntry.getName()));
+
+ int length;
+ while((length = zipInputStream.read(buffer)) > 0)
+ {
+ zipOutputStream.write(buffer, 0, length);
+ }
+
+ zipInputStream.closeEntry();
+ }
+ else
+ {
+ ////////////////////////////////////////////////////////////////////////////////////
+ // else - this is a sheet - so again, don't write it yet - stream its data below. //
+ ////////////////////////////////////////////////////////////////////////////////////
+ LOG.info("Skipping presumed sheet zip template entry [" + zipTemplateEntry.getName() + "] to output stream");
+ }
+ }
+
+ zipInputStream.close();
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error preparing to generate spreadsheet", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void createPivotTableTemplate(XSSFSheet pivotTableSheet, QReportView pivotView, XSSFSheet dataSheet, QReportView dataView) throws QReportingException
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // write just enough data to the dataView's sheet so that we can refer to it for creating the pivot table. //
+ // we need to do this, because POI will try to create the pivotCache referring to the data sheet, and if //
+ // there isn't any data there, it'll crash. //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ XSSFRow headerRow = dataSheet.createRow(0);
+ int columnNo = 0;
+ for(QReportField column : dataView.getColumns())
+ {
+ XSSFCell cell = headerRow.createCell(columnNo++);
+ // todo ... not like this
+ cell.setCellValue(QInstanceEnricher.nameToLabel(column.getName()));
+ }
+
+ XSSFRow valuesRow = dataSheet.createRow(1);
+ columnNo = 0;
+ for(QReportField column : dataView.getColumns())
+ {
+ XSSFCell cell = valuesRow.createCell(columnNo++);
+ cell.setCellValue("Value " + columnNo);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // for this template version of the pivot table, tell it there are only 2 rows in the source sheet //
+ // as that's all that we wrote above (a header and 1 fake value row) //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ int rows = 2;
+ String colsLetter = CellReference.convertNumToColString(dataView.getColumns().size() - 1);
+ AreaReference source = new AreaReference("A1:" + colsLetter + rows, SpreadsheetVersion.EXCEL2007);
+ CellReference position = new CellReference("A1");
+
+ //////////////////////////////////////////////////////////////////
+ // tell poi all about our pivot table - rows, cols, and columns //
+ //////////////////////////////////////////////////////////////////
+ XSSFPivotTable pivotTable = pivotTableSheet.createPivotTable(source, position, dataSheet);
+
+ for(PivotTableGroupBy row : CollectionUtils.nonNullList(pivotView.getPivotTableDefinition().getRows()))
+ {
+ int rowLabelColumnIndex = getColumnIndex(dataView.getColumns(), row.getFieldName());
+ pivotTable.addRowLabel(rowLabelColumnIndex);
+ }
+
+ for(PivotTableGroupBy column : CollectionUtils.nonNullList(pivotView.getPivotTableDefinition().getColumns()))
+ {
+ int colLabelColumnIndex = getColumnIndex(dataView.getColumns(), column.getFieldName());
+ pivotTable.addColLabel(colLabelColumnIndex);
+ }
+
+ for(PivotTableValue value : CollectionUtils.nonNullList(pivotView.getPivotTableDefinition().getValues()))
+ {
+ int columnLabelColumnIndex = getColumnIndex(dataView.getColumns(), value.getFieldName());
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // todo - some bug where, if use a group-by field here, then ... it doesn't get used for the grouping. //
+ // g-sheets does let me do this, so, maybe, download their file and see how it's different? //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////
+ pivotTable.addColumnLabel(DataConsolidateFunction.valueOf(value.getFunction().name()), columnLabelColumnIndex);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int getColumnIndex(List columns, String fieldName) throws QReportingException
+ {
+ for(int i = 0; i < columns.size(); i++)
+ {
+ if(columns.get(i).getName().equals(fieldName))
+ {
+ return (i);
+ }
+ }
+
+ throw (new QReportingException("Could not find column by name [" + fieldName + "]"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void createStyles(XSSFWorkbook workbook)
+ {
+ CreationHelper createHelper = workbook.getCreationHelper();
+
+ XSSFCellStyle dateStyle = workbook.createCellStyle();
+ dateStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd"));
+ styles.put("date", dateStyle);
+
+ XSSFCellStyle dateTimeStyle = workbook.createCellStyle();
+ dateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd H:mm:ss"));
+ styles.put("datetime", dateTimeStyle);
+
+ styles.put("title", poiExcelStylerInterface.createStyleForTitle(workbook, createHelper));
+ styles.put("header", poiExcelStylerInterface.createStyleForHeader(workbook, createHelper));
+ styles.put("footer", poiExcelStylerInterface.createStyleForFooter(workbook, createHelper));
+
+ XSSFCellStyle footerDateStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper);
+ footerDateStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd"));
+ styles.put("footer-date", footerDateStyle);
+
+ XSSFCellStyle footerDateTimeStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper);
+ footerDateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd H:mm:ss"));
+ styles.put("footer-datetime", footerDateTimeStyle);
+ }
+
+
+
+ /*******************************************************************************
+ ** Starts a new worksheet in the current workbook. Can be called multiple times.
+ *******************************************************************************/
+ @Override
+ public void start(ExportInput exportInput, List fields, String label, QReportView view) throws QReportingException
+ {
+ try
+ {
+ /////////////////////////////////////////
+ // close previous sheet if one is open //
+ /////////////////////////////////////////
+ closeLastSheetIfOpen();
+
+ if(currentView != null)
+ {
+ this.rowsPerView.put(currentView.getName(), rowNo);
+ }
+
+ this.currentView = view;
+ this.exportInput = exportInput;
+ this.fields = fields;
+ this.rowNo = 0;
+
+ this.fieldsPerView.put(view.getName(), fields);
+
+ //////////////////////////////////////////
+ // start the new sheet as: //
+ // - a new entry in the zipOutputStream //
+ // - with a new output stream writer //
+ // - and with a SpreadsheetWriter //
+ //////////////////////////////////////////
+ zipOutputStream.putNextEntry(new ZipEntry("xl/worksheets/sheet" + this.sheetIndex++ + ".xml"));
+ activeSheetWriter = new OutputStreamWriter(zipOutputStream);
+ sheetWriter = new StreamedPoiSheetWriter(activeSheetWriter);
+
+ if(ReportType.PIVOT.equals(view.getType()))
+ {
+ writePivotTable(view, ReportUtils.getSourceViewForPivotTableView(views, view));
+ }
+ else
+ {
+ sheetWriter.beginSheet();
+
+ ////////////////////////////////////////////////
+ // put the title and header rows in the sheet //
+ ////////////////////////////////////////////////
+ writeTitleAndHeader();
+ }
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error starting worksheet", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void writeTitleAndHeader() throws QReportingException
+ {
+ try
+ {
+ ///////////////
+ // title row //
+ ///////////////
+ if(StringUtils.hasContent(exportInput.getTitleRow()))
+ {
+ sheetWriter.insertRow(rowNo++);
+ sheetWriter.createCell(0, exportInput.getTitleRow(), styles.get("title").getIndex());
+ sheetWriter.endRow();
+ }
+
+ ////////////////
+ // header row //
+ ////////////////
+ if(exportInput.getIncludeHeaderRow())
+ {
+ sheetWriter.insertRow(rowNo++);
+ XSSFCellStyle headerStyle = styles.get("header");
+
+ int col = 0;
+ for(QFieldMetaData column : fields)
+ {
+ sheetWriter.createCell(col, column.getLabel(), headerStyle.getIndex());
+ col++;
+ }
+
+ sheetWriter.endRow();
+ }
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error starting Excel report"));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void addRecords(List qRecords) throws QReportingException
+ {
+ LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
+
+ try
+ {
+ for(QRecord qRecord : qRecords)
+ {
+ writeRecord(qRecord);
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.error("Exception generating excel file", e);
+ try
+ {
+ outputStream.close();
+ }
+ catch(IOException ex)
+ {
+ LOG.warn("Secondary error closing excel output stream", e);
+ }
+
+ throw (new QReportingException("Error generating Excel report", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void writeRecord(QRecord qRecord) throws IOException
+ {
+ writeRecord(qRecord, false);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void writeRecord(QRecord qRecord, boolean isFooter) throws IOException
+ {
+ sheetWriter.insertRow(rowNo++);
+
+ int styleIndex = -1;
+ int dateStyleIndex = styles.get("date").getIndex();
+ int dateTimeStyleIndex = styles.get("datetime").getIndex();
+ if(isFooter)
+ {
+ styleIndex = styles.get("footer").getIndex();
+ dateStyleIndex = styles.get("footer-date").getIndex();
+ dateTimeStyleIndex = styles.get("footer-datetime").getIndex();
+ }
+
+ int col = 0;
+ for(QFieldMetaData field : fields)
+ {
+ Serializable value = qRecord.getValue(field.getName());
+
+ if(value != null)
+ {
+ if(value instanceof String s)
+ {
+ sheetWriter.createCell(col, s, styleIndex);
+ }
+ else if(value instanceof Number n)
+ {
+ sheetWriter.createCell(col, n.doubleValue(), styleIndex);
+
+ if(excelCellFormats != null)
+ {
+ String format = excelCellFormats.get(field.getName());
+ if(format != null)
+ {
+ // todo - formats...
+ // worksheet.style(rowNo, col).format(format).set();
+ }
+ }
+ }
+ else if(value instanceof Boolean b)
+ {
+ sheetWriter.createCell(col, b, styleIndex);
+ }
+ else if(value instanceof Date d)
+ {
+ sheetWriter.createCell(col, DateUtil.getExcelDate(d), dateStyleIndex);
+ }
+ else if(value instanceof LocalDate d)
+ {
+ sheetWriter.createCell(col, DateUtil.getExcelDate(d), dateStyleIndex);
+ }
+ else if(value instanceof LocalDateTime d)
+ {
+ sheetWriter.createCell(col, DateUtil.getExcelDate(d), dateStyleIndex);
+ }
+ else if(value instanceof ZonedDateTime d)
+ {
+ sheetWriter.createCell(col, DateUtil.getExcelDate(d.toLocalDateTime()), dateTimeStyleIndex);
+ }
+ else if(value instanceof Instant i)
+ {
+ // todo - what would be a better zone to use here?
+ sheetWriter.createCell(col, DateUtil.getExcelDate(i.atZone(ZoneId.systemDefault()).toLocalDateTime()), dateTimeStyleIndex);
+ }
+ else
+ {
+ sheetWriter.createCell(col, ValueUtils.getValueAsString(value), styleIndex);
+ }
+ }
+
+ col++;
+ }
+
+ sheetWriter.endRow();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void addTotalsRow(QRecord record) throws QReportingException
+ {
+ try
+ {
+ writeRecord(record, true);
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error adding totals row", e));
+ }
+
+ /* todo
+ CellStyle totalsStyle = workbook.createCellStyle();
+ Font font = workbook.createFont();
+ font.setBold(true);
+ totalsStyle.setFont(font);
+ totalsStyle.setBorderTop(BorderStyle.THIN);
+ totalsStyle.setBorderTop(BorderStyle.THIN);
+ totalsStyle.setBorderBottom(BorderStyle.DOUBLE);
+
+ row.cellIterator().forEachRemaining(cell -> cell.setCellStyle(totalsStyle));
+ */
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void finish() throws QReportingException
+ {
+ try
+ {
+ //////////////////////////////////////////////
+ // close the last open sheet if one is open //
+ //////////////////////////////////////////////
+ closeLastSheetIfOpen();
+
+ /////////////////////////////
+ // close the output stream //
+ /////////////////////////////
+ zipOutputStream.close();
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error finishing Excel report", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void closeLastSheetIfOpen() throws IOException
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we have an active sheet writer: //
+ // - end the current sheet in the spreadsheet writer (write some closing xml, unless it's a pivot!) //
+ // - flush the contents through the activeSheetWriter //
+ // - close the zip entry in the output stream. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(activeSheetWriter != null)
+ {
+ if(!ReportType.PIVOT.equals(currentView.getType()))
+ {
+ sheetWriter.endSheet();
+ }
+
+ activeSheetWriter.flush();
+ zipOutputStream.closeEntry();
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** display formats is a map of field name to Excel format strings (e.g., $#,##0.00)
+ *******************************************************************************/
+ @Override
+ public void setDisplayFormats(Map displayFormats)
+ {
+ this.excelCellFormats = new HashMap<>();
+ for(Map.Entry entry : displayFormats.entrySet())
+ {
+ String excelFormat = DisplayFormat.getExcelFormat(entry.getValue());
+ if(excelFormat != null)
+ {
+ excelCellFormats.put(entry.getKey(), excelFormat);
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void writePivotTable(QReportView pivotTableView, QReportView dataView) throws QReportingException
+ {
+ try
+ {
+ //////////////////////////////////////////////////////////////////////////////////
+ // write the xml file that is the pivot table sheet. //
+ // note that the ZipEntry here will have been started above in the start method //
+ //////////////////////////////////////////////////////////////////////////////////
+ activeSheetWriter.write("""
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+ activeSheetWriter.flush();
+
+ ////////////////////////////////////////////////////////////////////////////
+ // start a new zip entry, for this pivot view's cacheDefinition reference //
+ ////////////////////////////////////////////////////////////////////////////
+ zipOutputStream.putNextEntry(new ZipEntry(pivotViewToCacheDefinitionReferenceMap.get(pivotTableView.getName())));
+
+ /////////////////////////////////////////////////////////
+ // prepare the xml for each field (e.g., w/ its labelO //
+ /////////////////////////////////////////////////////////
+ List cachedFieldElements = new ArrayList<>();
+ for(QFieldMetaData column : this.fieldsPerView.get(dataView.getName()))
+ {
+ cachedFieldElements.add(String.format("""
+
+
+
+ """, column.getLabel()));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////
+ // write the xml file that is the pivot cache definition (structure only, no data) //
+ /////////////////////////////////////////////////////////////////////////////////////
+ activeSheetWriter = new OutputStreamWriter(zipOutputStream);
+ activeSheetWriter.write(String.format("""
+
+
+
+
+
+
+ %s
+
+
+ """, CellReference.convertNumToColString(dataView.getColumns().size() - 1), rowsPerView.get(dataView.getName()), dataView.getColumns().size(), StringUtils.join("\n", cachedFieldElements)));
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error writing pivot table", e));
+ }
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PlainPoiExcelStyler.java
similarity index 86%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PlainPoiExcelStyler.java
index 3fdd189a..cafb5a30 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PlainPoiExcelStyler.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2022. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,13 +19,13 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
/*******************************************************************************
** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
*******************************************************************************/
-public class PlainExcelStyler implements ExcelStylerInterface
+public class PlainPoiExcelStyler implements PoiExcelStylerInterface
{
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PoiExcelStylerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PoiExcelStylerInterface.java
new file mode 100644
index 00000000..c052e194
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PoiExcelStylerInterface.java
@@ -0,0 +1,59 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
+
+
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+
+/*******************************************************************************
+ ** Interface for classes that know how to apply styles to an Excel stream being
+ ** built by POI.
+ *******************************************************************************/
+public interface PoiExcelStylerInterface
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default XSSFCellStyle createStyleForTitle(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ return (workbook.createCellStyle());
+ }
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ return (workbook.createCellStyle());
+ }
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default XSSFCellStyle createStyleForFooter(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ return (workbook.createCellStyle());
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedPoiSheetWriter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedPoiSheetWriter.java
new file mode 100644
index 00000000..1eace3fd
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedPoiSheetWriter.java
@@ -0,0 +1,205 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
+
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.regex.Pattern;
+import org.apache.poi.ss.util.CellReference;
+
+
+/*******************************************************************************
+ ** Write excel formatted XML to a Writer.
+ ** Originally from https://coderanch.com/t/548897/java/Generate-large-excel-POI
+ *******************************************************************************/
+public class StreamedPoiSheetWriter
+{
+ private static Pattern xmlSpecialChars = Pattern.compile(".*[&<>'\"].*");
+
+ private final Writer writer;
+ private int rowNo;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public StreamedPoiSheetWriter(Writer writer)
+ {
+ this.writer = writer;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void beginSheet() throws IOException
+ {
+ writer.write("" +
+ "");
+ writer.write("\n");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void endSheet() throws IOException
+ {
+ writer.write("");
+ writer.write("");
+ }
+
+
+
+ /*******************************************************************************
+ ** Insert a new row
+ **
+ ** @param rowNo 0-based row number
+ *******************************************************************************/
+ public void insertRow(int rowNo) throws IOException
+ {
+ writer.write("\n");
+ this.rowNo = rowNo;
+ }
+
+
+
+ /*******************************************************************************
+ ** Insert row end marker
+ *******************************************************************************/
+ public void endRow() throws IOException
+ {
+ writer.write("
\n");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, String value, int styleIndex) throws IOException
+ {
+ String ref = new CellReference(rowNo, columnIndex).formatAsString();
+ writer.write("");
+ writer.write("" + cleanseValue(value) + "");
+ writer.write("");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String cleanseValue(String value)
+ {
+ // todo - profile...
+ if(xmlSpecialChars.matcher(value).find())
+ {
+ value = value.replace("&", "&");
+ value = value.replace("<", "<");
+ value = value.replace(">", ">");
+ value = value.replace("'", "'");
+ value = value.replace("\"", """);
+ }
+ return (value);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, String value) throws IOException
+ {
+ createCell(columnIndex, value, -1);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, double value, int styleIndex) throws IOException
+ {
+ String ref = new CellReference(rowNo, columnIndex).formatAsString();
+ writer.write("");
+ writer.write("" + value + "");
+ writer.write("");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, double value) throws IOException
+ {
+ createCell(columnIndex, value, -1);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, Boolean value) throws IOException
+ {
+ createCell(columnIndex, value, -1);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, Boolean value, int styleIndex) throws IOException
+ {
+ String ref = new CellReference(rowNo, columnIndex).formatAsString();
+ writer.write("");
+ writer.write("" + value + "");
+ writer.write("");
+ }
+
+ // todo? public void createCell(int columnIndex, Calendar value, int styleIndex) throws IOException
+ // todo? {
+ // todo? createCell(columnIndex, DateUtil.getExcelDate(value, false), styleIndex);
+ // todo? }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableDefinition.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableDefinition.java
new file mode 100644
index 00000000..541b537b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableDefinition.java
@@ -0,0 +1,176 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.pivottable;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/*******************************************************************************
+ ** Full definition of a pivot table - its rows, columns, and values.
+ *******************************************************************************/
+public class PivotTableDefinition
+{
+ private List rows;
+ private List columns;
+ private List values;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public PivotTableDefinition withRow(PivotTableGroupBy row)
+ {
+ if(this.rows == null)
+ {
+ this.rows = new ArrayList<>();
+ }
+ this.rows.add(row);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public PivotTableDefinition withColumn(PivotTableGroupBy column)
+ {
+ if(this.columns == null)
+ {
+ this.columns = new ArrayList<>();
+ }
+ this.columns.add(column);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public PivotTableDefinition withValue(PivotTableValue value)
+ {
+ if(this.values == null)
+ {
+ this.values = new ArrayList<>();
+ }
+ this.values.add(value);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for rows
+ *******************************************************************************/
+ public List getRows()
+ {
+ return (this.rows);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for rows
+ *******************************************************************************/
+ public void setRows(List rows)
+ {
+ this.rows = rows;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for rows
+ *******************************************************************************/
+ public PivotTableDefinition withRows(List rows)
+ {
+ this.rows = rows;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for columns
+ *******************************************************************************/
+ public List getColumns()
+ {
+ return (this.columns);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for columns
+ *******************************************************************************/
+ public void setColumns(List columns)
+ {
+ this.columns = columns;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for columns
+ *******************************************************************************/
+ public PivotTableDefinition withColumns(List columns)
+ {
+ this.columns = columns;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for values
+ *******************************************************************************/
+ public List getValues()
+ {
+ return (this.values);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for values
+ *******************************************************************************/
+ public void setValues(List values)
+ {
+ this.values = values;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for values
+ *******************************************************************************/
+ public PivotTableDefinition withValues(List values)
+ {
+ this.values = values;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableFunction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableFunction.java
new file mode 100644
index 00000000..23940202
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableFunction.java
@@ -0,0 +1,65 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.pivottable;
+
+
+/*******************************************************************************
+ ** Functions that can be applied to Values in a pivot table.
+ *******************************************************************************/
+public enum PivotTableFunction
+{
+ AVERAGE("Average"),
+ COUNT("Count Numbers (COUNTA)"),
+ COUNT_NUMS("Count Values (COUNT)"),
+ MAX("Max"),
+ MIN("Min"),
+ PRODUCT("Product"),
+ STD_DEV("StdDev"),
+ STD_DEVP("StdDevp"),
+ SUM("Sum"),
+ VAR("Var"),
+ VARP("Varp");
+
+
+ private final String label;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ PivotTableFunction(String label)
+ {
+ this.label = label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for label
+ **
+ *******************************************************************************/
+ public String getLabel()
+ {
+ return label;
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableGroupBy.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableGroupBy.java
new file mode 100644
index 00000000..f3224a6d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableGroupBy.java
@@ -0,0 +1,128 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.pivottable;
+
+
+/*******************************************************************************
+ ** Either a row or column grouping in a pivot table. e.g., a field plus
+ ** sorting details, plus showTotals boolean.
+ *******************************************************************************/
+public class PivotTableGroupBy
+{
+ private String fieldName;
+ private PivotTableOrderBy orderBy;
+ private boolean showTotals;
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldName
+ *******************************************************************************/
+ public String getFieldName()
+ {
+ return (this.fieldName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldName
+ *******************************************************************************/
+ public void setFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldName
+ *******************************************************************************/
+ public PivotTableGroupBy withFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for orderBy
+ *******************************************************************************/
+ public PivotTableOrderBy getOrderBy()
+ {
+ return (this.orderBy);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for orderBy
+ *******************************************************************************/
+ public void setOrderBy(PivotTableOrderBy orderBy)
+ {
+ this.orderBy = orderBy;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for orderBy
+ *******************************************************************************/
+ public PivotTableGroupBy withOrderBy(PivotTableOrderBy orderBy)
+ {
+ this.orderBy = orderBy;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for showTotals
+ *******************************************************************************/
+ public boolean getShowTotals()
+ {
+ return (this.showTotals);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for showTotals
+ *******************************************************************************/
+ public void setShowTotals(boolean showTotals)
+ {
+ this.showTotals = showTotals;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for showTotals
+ *******************************************************************************/
+ public PivotTableGroupBy withShowTotals(boolean showTotals)
+ {
+ this.showTotals = showTotals;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableOrderBy.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableOrderBy.java
new file mode 100644
index 00000000..62d02c48
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableOrderBy.java
@@ -0,0 +1,31 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.pivottable;
+
+
+/*******************************************************************************
+ ** How a group-by (rows or columns) should be sorted.
+ *******************************************************************************/
+public class PivotTableOrderBy
+{
+ // todo - implement, but only if POI supports (or we build our own support...)
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableValue.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableValue.java
new file mode 100644
index 00000000..3982fcf2
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/pivottable/PivotTableValue.java
@@ -0,0 +1,94 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.pivottable;
+
+
+/*******************************************************************************
+ ** a value (e.g., field name + function) used in a pivot table
+ *******************************************************************************/
+public class PivotTableValue
+{
+ private String fieldName;
+ private PivotTableFunction function;
+
+
+ /*******************************************************************************
+ ** Getter for fieldName
+ *******************************************************************************/
+ public String getFieldName()
+ {
+ return (this.fieldName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldName
+ *******************************************************************************/
+ public void setFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldName
+ *******************************************************************************/
+ public PivotTableValue withFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for function
+ *******************************************************************************/
+ public PivotTableFunction getFunction()
+ {
+ return (this.function);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for function
+ *******************************************************************************/
+ public void setFunction(PivotTableFunction function)
+ {
+ this.function = function;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for function
+ *******************************************************************************/
+ public PivotTableValue withFunction(PivotTableFunction function)
+ {
+ this.function = function;
+ return (this);
+ }
+
+}