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); + } + +}