From b863d626887a5971aa9dd6e0e24da3f34dd071dc Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 18 Mar 2025 10:42:53 -0500 Subject: [PATCH] Add style customizer to report action, with excel poi implementation for columnWidths, more cell styles, merged ranges --- .../reporting/GenerateReportAction.java | 5 ++ .../ExcelPoiBasedStreamingExportStreamer.java | 60 ++++++++++++-- ...asedStreamingStyleCustomizerInterface.java | 81 +++++++++++++++++++ .../reporting/GenerateReportActionTest.java | 30 +++++++ 4 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingStyleCustomizerInterface.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index 07863fd0..eacc31ec 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -168,6 +168,11 @@ public class GenerateReportAction extends AbstractQActionFunction excelCellFormats; - private Map styles = new HashMap<>(); + private Map styles = new HashMap<>(); private int rowNo = 0; private int sheetIndex = 1; @@ -402,6 +405,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter dateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT)); styles.put("datetime", dateTimeStyle); + PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface(); styles.put("title", poiExcelStylerInterface.createStyleForTitle(workbook, createHelper)); styles.put("header", poiExcelStylerInterface.createStyleForHeader(workbook, createHelper)); styles.put("footer", poiExcelStylerInterface.createStyleForFooter(workbook, createHelper)); @@ -413,6 +417,11 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter XSSFCellStyle footerDateTimeStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper); footerDateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT)); styles.put("footer-datetime", footerDateTimeStyle); + + if(styleCustomizerInterface != null) + { + styleCustomizerInterface.customizeStyles(styles, workbook, createHelper); + } } @@ -458,7 +467,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter } else { - sheetWriter.beginSheet(); + sheetWriter.beginSheet(view, styleCustomizerInterface); //////////////////////////////////////////////// // put the title and header rows in the sheet // @@ -560,6 +569,16 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter + /*************************************************************************** + ** + ***************************************************************************/ + public static void setStyleForField(QRecord record, String fieldName, String styleName) + { + record.setDisplayValue(fieldName + ":excelStyle", styleName); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -567,12 +586,12 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter { sheetWriter.insertRow(rowNo++); - int styleIndex = -1; + int baseStyleIndex = -1; int dateStyleIndex = styles.get("date").getIndex(); int dateTimeStyleIndex = styles.get("datetime").getIndex(); if(isFooter) { - styleIndex = styles.get("footer").getIndex(); + baseStyleIndex = styles.get("footer").getIndex(); dateStyleIndex = styles.get("footer-date").getIndex(); dateTimeStyleIndex = styles.get("footer-datetime").getIndex(); } @@ -582,6 +601,13 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter { Serializable value = qRecord.getValue(field.getName()); + String overrideStyleName = qRecord.getDisplayValue(field.getName() + ":excelStyle"); + int styleIndex = baseStyleIndex; + if(overrideStyleName != null) + { + styleIndex = styles.get(overrideStyleName).getIndex(); + } + if(value != null) { if(value instanceof String s) @@ -706,7 +732,7 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter { if(!ReportType.PIVOT.equals(currentView.getType())) { - sheetWriter.endSheet(); + sheetWriter.endSheet(currentView, styleCustomizerInterface); } activeSheetWriter.flush(); @@ -815,7 +841,29 @@ public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInter *******************************************************************************/ protected PoiExcelStylerInterface getStylerInterface() { + if(styleCustomizerInterface != null) + { + return styleCustomizerInterface.getExcelStyler(); + } + return (new PlainPoiExcelStyler()); } + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void setExportStyleCustomizer(ExportStyleCustomizerInterface exportStyleCustomizer) + { + if(exportStyleCustomizer instanceof ExcelPoiBasedStreamingStyleCustomizerInterface poiExcelStylerInterface) + { + this.styleCustomizerInterface = poiExcelStylerInterface; + } + else + { + LOG.debug("Supplied export style customizer is not an instance of ExcelPoiStyleCustomizerInterface, so will not be used for an excel export", logPair("exportStyleCustomizerClass", exportStyleCustomizer.getClass().getSimpleName())); + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingStyleCustomizerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingStyleCustomizerInterface.java new file mode 100644 index 00000000..251e5ff7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingStyleCustomizerInterface.java @@ -0,0 +1,81 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.reporting.ExportStyleCustomizerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + + +/******************************************************************************* + ** style customization points for Excel files generated via our streaming POI. + *******************************************************************************/ +public interface ExcelPoiBasedStreamingStyleCustomizerInterface extends ExportStyleCustomizerInterface +{ + /*************************************************************************** + ** slightly legacy way we did excel styles - but get an instance of object + ** that defaults "default" styles (header, footer, etc). + ***************************************************************************/ + default PoiExcelStylerInterface getExcelStyler() + { + return (new PlainPoiExcelStyler()); + } + + + /*************************************************************************** + ** either change "default" styles put in the styles map, or create new ones + ** which can then be applied to row/field values (cells) via: + ** ExcelPoiBasedStreamingExportStreamer.setStyleForField(row, fieldName, styleName); + ***************************************************************************/ + default void customizeStyles(Map styles, XSSFWorkbook workbook, CreationHelper createHelper) + { + ////////////////// + // noop default // + ////////////////// + } + + + /*************************************************************************** + ** for a given view (sheet), return a list of custom column widths. + ** any nulls in the list are ignored (so default width is used). + ***************************************************************************/ + default List getColumnWidthsForView(QReportView view) + { + return (null); + } + + + /*************************************************************************** + ** for a given view (sheet), return a list of any ranges which should be + ** merged, as in "A1:C1" (first three cells in first row). + ***************************************************************************/ + default List getMergedRangesForView(QReportView view) + { + return (null); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java index c2594727..48d75f5e 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java @@ -38,6 +38,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.reporting.excel.TestExcelStyler; import com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel.ExcelFastexcelExportStreamer; import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.BoldHeaderAndFooterPoiExcelStyler; import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.ExcelPoiBasedStreamingExportStreamer; @@ -56,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; 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.fields.QFieldType; @@ -490,6 +492,34 @@ public class GenerateReportActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void runXlsxWithStyleCustomizer() throws Exception + { + ReportFormat format = ReportFormat.XLSX; + String name = "/tmp/report-customized.xlsx"; + try(FileOutputStream fileOutputStream = new FileOutputStream(name)) + { + QInstance qInstance = QContext.getQInstance(); + qInstance.addReport(defineTableOnlyReport()); + insertPersonRecords(qInstance); + + ReportInput reportInput = new ReportInput(); + reportInput.setReportName(REPORT_NAME); + reportInput.setReportDestination(new ReportDestination().withReportFormat(format).withReportOutputStream(fileOutputStream)); + reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now())); + reportInput.setExportStyleCustomizer(new QCodeReference(TestExcelStyler.class)); + new GenerateReportAction().execute(reportInput); + System.out.println("Wrote File: " + name); + + LocalMacDevUtils.openFile(name); + } + } + + + /******************************************************************************* ** *******************************************************************************/