Initial basic working version of POI pure-streaming excel generation, with pivot tables.

- moved fastexcel to dedicated sub-package
This commit is contained in:
2024-03-27 19:46:22 -05:00
parent 9257519462
commit 05f31d0722
14 changed files with 1663 additions and 25 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
{

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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,18 +54,18 @@ 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<QFieldMetaData> fields;
private OutputStream outputStream;
private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
private FastExcelStylerInterface fastExcelStylerInterface = new PlainFastExcelStyler();
private Map<String, String> excelCellFormats;
private Workbook workbook;
@ -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<QFieldMetaData> fields, String label) throws QReportingException
public void start(ExportInput exportInput, List<QFieldMetaData> 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();
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
{
/*******************************************************************************

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
{
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QReportView> views;
private ExportInput exportInput;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
private ZipOutputStream zipOutputStream;
private PoiExcelStylerInterface poiExcelStylerInterface = new PlainPoiExcelStyler();
private Map<String, String> excelCellFormats;
private int rowNo = 0;
private int sheetIndex = 1;
private Map<String, String> pivotViewToCacheDefinitionReferenceMap = new HashMap<>();
private Map<String, XSSFCellStyle> styles = new HashMap<>();
private Writer activeSheetWriter = null;
private StreamedPoiSheetWriter sheetWriter = null;
private QReportView currentView = null;
private Map<String, List<QFieldMetaData>> fieldsPerView = new HashMap<>();
private Map<String, Integer> rowsPerView = new HashMap<>();
/*******************************************************************************
**
*******************************************************************************/
public ExcelPoiBasedStreamingExportStreamer()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void preRun(ReportDestination reportDestination, List<QReportView> 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<String, XSSFSheet> sheetMapByExcelReference = new HashMap<>();
Map<String, XSSFSheet> 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<String> 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<String> 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<QReportField> 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<QFieldMetaData> 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<QRecord> 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<String, String> displayFormats)
{
this.excelCellFormats = new HashMap<>();
for(Map.Entry<String, String> 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("""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main">
<sheetPr>
<outlinePr summaryBelow="0" summaryRight="0"/>
</sheetPr>
<sheetViews>
<sheetView workbookViewId="0"/>
</sheetViews>
<sheetFormatPr customHeight="1" defaultColWidth="14.43" defaultRowHeight="15.0"/>
<sheetData>
<row r="1"/>
</sheetData>
</worksheet>
""");
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<String> cachedFieldElements = new ArrayList<>();
for(QFieldMetaData column : this.fieldsPerView.get(dataView.getName()))
{
cachedFieldElements.add(String.format("""
<cacheField numFmtId="0" name="%s">
<sharedItems/>
</cacheField>
""", column.getLabel()));
}
/////////////////////////////////////////////////////////////////////////////////////
// write the xml file that is the pivot cache definition (structure only, no data) //
/////////////////////////////////////////////////////////////////////////////////////
activeSheetWriter = new OutputStreamWriter(zipOutputStream);
activeSheetWriter.write(String.format("""
<?xml version="1.0" encoding="UTF-8"?>
<pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" createdVersion="3" minRefreshableVersion="3" refreshedVersion="3" refreshedBy="Apache POI" refreshedDate="1.7113> 95767702E12" refreshOnLoad="true" r:id="rId1">
<cacheSource type="worksheet">
<worksheetSource sheet="table1" ref="A1:%s%d"/>
</cacheSource>
<cacheFields count="%d">
%s
</cacheFields>
</pivotCacheDefinition>
""", 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));
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
{
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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());
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">");
writer.write("<sheetData>\n");
}
/*******************************************************************************
**
*******************************************************************************/
public void endSheet() throws IOException
{
writer.write("</sheetData>");
writer.write("</worksheet>");
}
/*******************************************************************************
** Insert a new row
**
** @param rowNo 0-based row number
*******************************************************************************/
public void insertRow(int rowNo) throws IOException
{
writer.write("<row r=\"" + (rowNo + 1) + "\">\n");
this.rowNo = rowNo;
}
/*******************************************************************************
** Insert row end marker
*******************************************************************************/
public void endRow() throws IOException
{
writer.write("</row>\n");
}
/*******************************************************************************
**
*******************************************************************************/
public void createCell(int columnIndex, String value, int styleIndex) throws IOException
{
String ref = new CellReference(rowNo, columnIndex).formatAsString();
writer.write("<c r=\"" + ref + "\" t=\"inlineStr\"");
if(styleIndex != -1)
{
writer.write(" s=\"" + styleIndex + "\"");
}
writer.write(">");
writer.write("<is><t>" + cleanseValue(value) + "</t></is>");
writer.write("</c>");
}
/*******************************************************************************
**
*******************************************************************************/
private String cleanseValue(String value)
{
// todo - profile...
if(xmlSpecialChars.matcher(value).find())
{
value = value.replace("&", "&amp;");
value = value.replace("<", "&lt;");
value = value.replace(">", "&gt;");
value = value.replace("'", "&apos;");
value = value.replace("\"", "&quot;");
}
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("<c r=\"" + ref + "\" t=\"n\"");
if(styleIndex != -1)
{
writer.write(" s=\"" + styleIndex + "\"");
}
writer.write(">");
writer.write("<v>" + value + "</v>");
writer.write("</c>");
}
/*******************************************************************************
**
*******************************************************************************/
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("<c r=\"" + ref + "\" t=\"n\"");
if(styleIndex != -1)
{
writer.write(" s=\"" + styleIndex + "\"");
}
writer.write(">");
writer.write("<v>" + value + "</v>");
writer.write("</c>");
}
// todo? public void createCell(int columnIndex, Calendar value, int styleIndex) throws IOException
// todo? {
// todo? createCell(columnIndex, DateUtil.getExcelDate(value, false), styleIndex);
// todo? }
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<PivotTableGroupBy> rows;
private List<PivotTableGroupBy> columns;
private List<PivotTableValue> 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<PivotTableGroupBy> getRows()
{
return (this.rows);
}
/*******************************************************************************
** Setter for rows
*******************************************************************************/
public void setRows(List<PivotTableGroupBy> rows)
{
this.rows = rows;
}
/*******************************************************************************
** Fluent setter for rows
*******************************************************************************/
public PivotTableDefinition withRows(List<PivotTableGroupBy> rows)
{
this.rows = rows;
return (this);
}
/*******************************************************************************
** Getter for columns
*******************************************************************************/
public List<PivotTableGroupBy> getColumns()
{
return (this.columns);
}
/*******************************************************************************
** Setter for columns
*******************************************************************************/
public void setColumns(List<PivotTableGroupBy> columns)
{
this.columns = columns;
}
/*******************************************************************************
** Fluent setter for columns
*******************************************************************************/
public PivotTableDefinition withColumns(List<PivotTableGroupBy> columns)
{
this.columns = columns;
return (this);
}
/*******************************************************************************
** Getter for values
*******************************************************************************/
public List<PivotTableValue> getValues()
{
return (this.values);
}
/*******************************************************************************
** Setter for values
*******************************************************************************/
public void setValues(List<PivotTableValue> values)
{
this.values = values;
}
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
public PivotTableDefinition withValues(List<PivotTableValue> values)
{
this.values = values;
return (this);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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...)
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}