From b05c5749b4edeb98b84a0aaff5c371750698243b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 14 Sep 2022 13:00:19 -0500 Subject: [PATCH] QQQ-42 initial implementation of qqq reports (pivots, WIP) --- ...rtStreamer.java => CsvExportStreamer.java} | 61 ++- ...Streamer.java => ExcelExportStreamer.java} | 195 +++++-- .../{ReportAction.java => ExportAction.java} | 80 +-- ...face.java => ExportStreamerInterface.java} | 27 +- .../actions/reporting/FormulaInterpreter.java | 273 ++++++++++ .../reporting/GenerateReportAction.java | 510 ++++++++++++++++++ .../reporting/ListOfMapsExportStreamer.java | 145 +++++ .../core/actions/reporting/PivotKey.java | 111 ++++ .../core/actions/values/QValueFormatter.java | 68 ++- .../core/exceptions/QFormulaException.java | 51 ++ .../QMetaDataVariableInterpreter.java | 65 ++- .../model/actions/reporting/ExportInput.java | 228 ++++++++ .../{ReportOutput.java => ExportOutput.java} | 4 +- .../model/actions/reporting/ReportFormat.java | 19 +- .../model/actions/reporting/ReportInput.java | 57 +- .../actions/tables/query/QFilterOrderBy.java | 33 +- .../core/model/metadata/QInstance.java | 62 +++ .../model/metadata/fields/DisplayFormat.java | 42 +- .../metadata/reporting/QReportField.java | 207 +++++++ .../metadata/reporting/QReportMetaData.java | 246 +++++++++ .../model/metadata/reporting/QReportView.java | 350 ++++++++++++ .../model/metadata/reporting/ReportType.java | 32 ++ .../memory/MemoryRecordStore.java | 130 ++++- .../qqq/backend/core/utils/Pair.java | 107 ++++ .../utils/aggregates/AggregatesInterface.java | 63 +++ .../aggregates/BigDecimalAggregates.java | 135 +++++ .../utils/aggregates/IntegerAggregates.java | 135 +++++ ...tActionTest.java => ExportActionTest.java} | 52 +- .../reporting/FormulaInterpreterTest.java | 143 +++++ .../reporting/GenerateReportActionTest.java | 442 +++++++++++++++ .../actions/values/QValueFormatterTest.java | 9 + .../QMetaDataVariableInterpreterTest.java | 38 ++ .../backend/core/testutils/PersonQRecord.java | 81 +++ .../qqq/backend/core/utils/TestUtils.java | 6 +- .../core/utils/aggregates/AggregatesTest.java | 125 +++++ .../javalin/QJavalinImplementation.java | 30 +- .../picocli/QPicoCliImplementation.java | 28 +- 37 files changed, 4141 insertions(+), 249 deletions(-) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/{CsvReportStreamer.java => CsvExportStreamer.java} (71%) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/{ExcelReportStreamer.java => ExcelExportStreamer.java} (50%) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/{ReportAction.java => ExportAction.java} (85%) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/{ReportStreamerInterface.java => ExportStreamerInterface.java} (69%) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QFormulaException.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ExportInput.java rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/{ReportOutput.java => ExportOutput.java} (95%) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Pair.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesInterface.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/BigDecimalAggregates.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/IntegerAggregates.java rename qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/{ReportActionTest.java => ExportActionTest.java} (83%) create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/testutils/PersonQRecord.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvReportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java similarity index 71% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvReportStreamer.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java index b6f73caf..057bc42b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvReportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java @@ -27,24 +27,25 @@ import java.nio.charset.StandardCharsets; import java.util.List; import com.kingsrook.qqq.backend.core.adapters.QRecordToCsvAdapter; import com.kingsrook.qqq.backend.core.exceptions.QReportingException; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /******************************************************************************* - ** CSV report format implementation + ** CSV export format implementation *******************************************************************************/ -public class CsvReportStreamer implements ReportStreamerInterface +public class CsvExportStreamer implements ExportStreamerInterface { - private static final Logger LOG = LogManager.getLogger(CsvReportStreamer.class); + private static final Logger LOG = LogManager.getLogger(CsvExportStreamer.class); private final QRecordToCsvAdapter qRecordToCsvAdapter; - private ReportInput reportInput; + private ExportInput exportInput; private QTableMetaData table; private List fields; private OutputStream outputStream; @@ -54,7 +55,7 @@ public class CsvReportStreamer implements ReportStreamerInterface /******************************************************************************* ** *******************************************************************************/ - public CsvReportStreamer() + public CsvExportStreamer() { qRecordToCsvAdapter = new QRecordToCsvAdapter(); } @@ -65,12 +66,12 @@ public class CsvReportStreamer implements ReportStreamerInterface ** *******************************************************************************/ @Override - public void start(ReportInput reportInput, List fields) throws QReportingException + public void start(ExportInput exportInput, List fields) throws QReportingException { - this.reportInput = reportInput; + this.exportInput = exportInput; this.fields = fields; - table = reportInput.getTable(); - outputStream = this.reportInput.getReportOutputStream(); + table = exportInput.getTable(); + outputStream = this.exportInput.getReportOutputStream(); writeReportHeaderRow(); } @@ -84,6 +85,11 @@ public class CsvReportStreamer implements ReportStreamerInterface { try { + if(StringUtils.hasContent(exportInput.getTitleRow())) + { + outputStream.write(exportInput.getTitleRow().getBytes(StandardCharsets.UTF_8)); + } + int col = 0; for(QFieldMetaData column : fields) { @@ -113,15 +119,25 @@ public class CsvReportStreamer implements ReportStreamerInterface List qRecords = recordPipe.consumeAvailableRecords(); LOG.info("Consuming [" + qRecords.size() + "] records from the pipe"); + for(QRecord qRecord : qRecords) + { + writeRecord(qRecord); + } + return (qRecords.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void writeRecord(QRecord qRecord) throws QReportingException + { try { - for(QRecord qRecord : qRecords) - { - String csv = qRecordToCsvAdapter.recordToCsv(table, qRecord, fields); - outputStream.write(csv.getBytes(StandardCharsets.UTF_8)); - outputStream.flush(); // todo - less often? - } - return (qRecords.size()); + String csv = qRecordToCsvAdapter.recordToCsv(table, qRecord, fields); + outputStream.write(csv.getBytes(StandardCharsets.UTF_8)); + outputStream.flush(); // todo - less often? } catch(Exception e) { @@ -131,6 +147,17 @@ public class CsvReportStreamer implements ReportStreamerInterface + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void addTotalsRow(QRecord record) throws QReportingException + { + writeRecord(record); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelReportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java similarity index 50% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelReportStreamer.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java index 004fe8a8..07a7ceef 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelReportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java @@ -28,41 +28,49 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QReportingException; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; 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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dhatim.fastexcel.BorderSide; +import org.dhatim.fastexcel.BorderStyle; import org.dhatim.fastexcel.Workbook; import org.dhatim.fastexcel.Worksheet; /******************************************************************************* - ** Excel report format implementation + ** Excel export format implementation *******************************************************************************/ -public class ExcelReportStreamer implements ReportStreamerInterface +public class ExcelExportStreamer implements ExportStreamerInterface { - private static final Logger LOG = LogManager.getLogger(ExcelReportStreamer.class); + private static final Logger LOG = LogManager.getLogger(ExcelExportStreamer.class); - private ReportInput reportInput; + private ExportInput exportInput; private QTableMetaData table; private List fields; private OutputStream outputStream; + private Map excelCellFormats; + private Workbook workbook; private Worksheet worksheet; - private int row = 1; + private int row = 0; /******************************************************************************* ** *******************************************************************************/ - public ExcelReportStreamer() + public ExcelExportStreamer() { } @@ -72,12 +80,31 @@ public class ExcelReportStreamer implements ReportStreamerInterface ** *******************************************************************************/ @Override - public void start(ReportInput reportInput, List fields) throws QReportingException + public void setDisplayFormats(Map displayFormats) { - this.reportInput = reportInput; + this.excelCellFormats = new HashMap<>(); + for(Map.Entry entry : displayFormats.entrySet()) + { + String excelFormat = DisplayFormat.getExcelFormat(entry.getValue()); + if(excelFormat != null) + { + excelCellFormats.put(entry.getKey(), excelFormat); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void start(ExportInput exportInput, List fields) throws QReportingException + { + this.exportInput = exportInput; this.fields = fields; - table = reportInput.getTable(); - outputStream = this.reportInput.getReportOutputStream(); + table = exportInput.getTable(); + outputStream = this.exportInput.getReportOutputStream(); workbook = new Workbook(outputStream, "QQQ", null); worksheet = workbook.newWorksheet("Sheet 1"); @@ -94,13 +121,32 @@ public class ExcelReportStreamer implements ReportStreamerInterface { try { + if(StringUtils.hasContent(exportInput.getTitleRow())) + { + worksheet.value(row, 0, exportInput.getTitleRow()); + worksheet.range(row, 0, row, fields.size() - 1).merge(); + worksheet.range(row, 0, row, fields.size() - 1).style() + .bold() + .fontSize(14) + .horizontalAlignment("center") + .set(); + row++; + } + int col = 0; for(QFieldMetaData column : fields) { - worksheet.value(0, col, column.getLabel()); + worksheet.value(row, col, column.getLabel()); col++; } + worksheet.range(row, 0, row, fields.size() - 1).style() + .bold() + .borderStyle(BorderSide.BOTTOM, BorderStyle.THIN) + .set(); + + row++; + worksheet.flush(); } catch(Exception e) @@ -124,51 +170,7 @@ public class ExcelReportStreamer implements ReportStreamerInterface { for(QRecord qRecord : qRecords) { - int col = 0; - for(QFieldMetaData column : fields) - { - Serializable value = qRecord.getValue(column.getName()); - if(value != null) - { - if(value instanceof String s) - { - worksheet.value(row, col, s); - } - else if(value instanceof Number n) - { - worksheet.value(row, col, n); - } - else if(value instanceof Boolean b) - { - worksheet.value(row, col, b); - } - else if(value instanceof Date d) - { - worksheet.value(row, col, d); - worksheet.style(row, col).format("yyyy-MM-dd").set(); - } - else if(value instanceof LocalDate d) - { - worksheet.value(row, col, d); - worksheet.style(row, col).format("yyyy-MM-dd").set(); - } - else if(value instanceof LocalDateTime d) - { - worksheet.value(row, col, d); - worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set(); - } - else if(value instanceof ZonedDateTime d) - { - worksheet.value(row, col, d); - worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set(); - } - else - { - worksheet.value(row, col, ValueUtils.getValueAsString(value)); - } - } - col++; - } + writeRecord(qRecord); row++; worksheet.flush(); // todo? not at all? or just sometimes? @@ -192,6 +194,85 @@ public class ExcelReportStreamer implements ReportStreamerInterface + /******************************************************************************* + ** + *******************************************************************************/ + private void writeRecord(QRecord qRecord) + { + int col = 0; + for(QFieldMetaData field : fields) + { + Serializable value = qRecord.getValue(field.getName()); + if(value != null) + { + if(value instanceof String s) + { + worksheet.value(row, col, s); + } + else if(value instanceof Number n) + { + worksheet.value(row, col, n); + + if(excelCellFormats != null) + { + String format = excelCellFormats.get(field.getName()); + if(format != null) + { + worksheet.style(row, col).format(format).set(); + } + } + } + else if(value instanceof Boolean b) + { + worksheet.value(row, col, b); + } + else if(value instanceof Date d) + { + worksheet.value(row, col, d); + worksheet.style(row, col).format("yyyy-MM-dd").set(); + } + else if(value instanceof LocalDate d) + { + worksheet.value(row, col, d); + worksheet.style(row, col).format("yyyy-MM-dd").set(); + } + else if(value instanceof LocalDateTime d) + { + worksheet.value(row, col, d); + worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set(); + } + else if(value instanceof ZonedDateTime d) + { + worksheet.value(row, col, d); + worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set(); + } + else + { + worksheet.value(row, col, ValueUtils.getValueAsString(value)); + } + } + col++; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addTotalsRow(QRecord record) + { + writeRecord(record); + + worksheet.range(row, 0, row, fields.size() - 1).style() + .bold() + .borderStyle(BorderSide.TOP, BorderStyle.THIN) + .borderStyle(BorderSide.BOTTOM, BorderStyle.DOUBLE) + .set(); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java similarity index 85% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportAction.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java index a63ae0d4..d15c591a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java @@ -35,9 +35,9 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QReportingException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; @@ -53,7 +53,7 @@ import org.apache.logging.log4j.Logger; /******************************************************************************* - ** Action to generate a report. + ** Action to generate an export from a table ** ** At this time (future may change?), this action starts a new thread to run ** the query in the backend module. As records are produced by the query, @@ -63,9 +63,9 @@ import org.apache.logging.log4j.Logger; ** time the report outputStream can be closed. ** *******************************************************************************/ -public class ReportAction +public class ExportAction { - private static final Logger LOG = LogManager.getLogger(ReportAction.class); + private static final Logger LOG = LogManager.getLogger(ExportAction.class); private boolean preExecuteRan = false; private Integer countFromPreExecute = null; @@ -82,21 +82,21 @@ public class ReportAction ** first, in their thread, to catch any validation errors before they start ** the thread (which they may abandon). *******************************************************************************/ - public void preExecute(ReportInput reportInput) throws QException + public void preExecute(ExportInput exportInput) throws QException { - ActionHelper.validateSession(reportInput); + ActionHelper.validateSession(exportInput); QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); - QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(reportInput.getBackend()); + QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(exportInput.getBackend()); /////////////////////////////////// // verify field names (if given) // /////////////////////////////////// - if(CollectionUtils.nullSafeHasContents(reportInput.getFieldNames())) + if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames())) { - QTableMetaData table = reportInput.getTable(); + QTableMetaData table = exportInput.getTable(); List badFieldNames = new ArrayList<>(); - for(String fieldName : reportInput.getFieldNames()) + for(String fieldName : exportInput.getFieldNames()) { try { @@ -119,8 +119,8 @@ public class ReportAction /////////////////////////////////////////////////////////////////////////////////////////////////////////// // check if this report format has a max-rows limit -- if so, do a count to verify we're under the limit // /////////////////////////////////////////////////////////////////////////////////////////////////////////// - ReportFormat reportFormat = reportInput.getReportFormat(); - verifyCountUnderMax(reportInput, backendModule, reportFormat); + ReportFormat reportFormat = exportInput.getReportFormat(); + verifyCountUnderMax(exportInput, backendModule, reportFormat); preExecuteRan = true; } @@ -130,28 +130,28 @@ public class ReportAction /******************************************************************************* ** Run the report. *******************************************************************************/ - public ReportOutput execute(ReportInput reportInput) throws QException + public ExportOutput execute(ExportInput exportInput) throws QException { if(!preExecuteRan) { ///////////////////////////////////// // ensure that pre-execute has ran // ///////////////////////////////////// - preExecute(reportInput); + preExecute(exportInput); } QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); - QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(reportInput.getBackend()); + QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(exportInput.getBackend()); ////////////////////////// // set up a query input // ////////////////////////// QueryInterface queryInterface = backendModule.getQueryInterface(); - QueryInput queryInput = new QueryInput(reportInput.getInstance()); - queryInput.setSession(reportInput.getSession()); - queryInput.setTableName(reportInput.getTableName()); - queryInput.setFilter(reportInput.getQueryFilter()); - queryInput.setLimit(reportInput.getLimit()); + QueryInput queryInput = new QueryInput(exportInput.getInstance()); + queryInput.setSession(exportInput.getSession()); + queryInput.setTableName(exportInput.getTableName()); + queryInput.setFilter(exportInput.getQueryFilter()); + queryInput.setLimit(exportInput.getLimit()); ///////////////////////////////////////////////////////////////// // tell this query that it needs to put its output into a pipe // @@ -162,9 +162,9 @@ public class ReportAction //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // set up a report streamer, which will read rows from the pipe, and write formatted report rows to the output stream // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ReportFormat reportFormat = reportInput.getReportFormat(); - ReportStreamerInterface reportStreamer = reportFormat.newReportStreamer(); - reportStreamer.start(reportInput, getFields(reportInput)); + ReportFormat reportFormat = exportInput.getReportFormat(); + ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer(); + reportStreamer.start(exportInput, getFields(exportInput)); ////////////////////////////////////////// // run the query action as an async job // @@ -251,17 +251,17 @@ public class ReportAction try { - reportInput.getReportOutputStream().close(); + exportInput.getReportOutputStream().close(); } catch(Exception e) { throw (new QReportingException("Error completing report", e)); } - ReportOutput reportOutput = new ReportOutput(); - reportOutput.setRecordCount(recordCount); + ExportOutput exportOutput = new ExportOutput(); + exportOutput.setRecordCount(recordCount); - return (reportOutput); + return (exportOutput); } @@ -269,12 +269,12 @@ public class ReportAction /******************************************************************************* ** *******************************************************************************/ - private List getFields(ReportInput reportInput) + private List getFields(ExportInput exportInput) { - QTableMetaData table = reportInput.getTable(); - if(reportInput.getFieldNames() != null) + QTableMetaData table = exportInput.getTable(); + if(exportInput.getFieldNames() != null) { - return (reportInput.getFieldNames().stream().map(table::getField).toList()); + return (exportInput.getFieldNames().stream().map(table::getField).toList()); } else { @@ -287,12 +287,12 @@ public class ReportAction /******************************************************************************* ** *******************************************************************************/ - private void verifyCountUnderMax(ReportInput reportInput, QBackendModuleInterface backendModule, ReportFormat reportFormat) throws QException + private void verifyCountUnderMax(ExportInput exportInput, QBackendModuleInterface backendModule, ReportFormat reportFormat) throws QException { if(reportFormat.getMaxCols() != null) { - List fields = getFields(reportInput); - if (fields.size() > reportFormat.getMaxCols()) + List fields = getFields(exportInput); + if(fields.size() > reportFormat.getMaxCols()) { throw (new QUserFacingException("The requested report would include more columns (" + String.format("%,d", fields.size()) + ") than the maximum allowed (" @@ -302,13 +302,13 @@ public class ReportAction if(reportFormat.getMaxRows() != null) { - if(reportInput.getLimit() == null || reportInput.getLimit() > reportFormat.getMaxRows()) + if(exportInput.getLimit() == null || exportInput.getLimit() > reportFormat.getMaxRows()) { CountInterface countInterface = backendModule.getCountInterface(); - CountInput countInput = new CountInput(reportInput.getInstance()); - countInput.setSession(reportInput.getSession()); - countInput.setTableName(reportInput.getTableName()); - countInput.setFilter(reportInput.getQueryFilter()); + CountInput countInput = new CountInput(exportInput.getInstance()); + countInput.setSession(exportInput.getSession()); + countInput.setTableName(exportInput.getTableName()); + countInput.setFilter(exportInput.getQueryFilter()); CountOutput countOutput = countInterface.execute(countInput); countFromPreExecute = countOutput.getCount(); if(countFromPreExecute > reportFormat.getMaxRows()) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportStreamerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java similarity index 69% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportStreamerInterface.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java index b75610c7..df0dfd72 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportStreamerInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java @@ -23,20 +23,22 @@ package com.kingsrook.qqq.backend.core.actions.reporting; import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QReportingException; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; /******************************************************************************* - ** Interface for various report formats to implement. + ** Interface for various export formats to implement. *******************************************************************************/ -public interface ReportStreamerInterface +public interface ExportStreamerInterface { /******************************************************************************* ** Called once, before any rows are available. Meant to write a header, for example. *******************************************************************************/ - void start(ReportInput reportInput, List fields) throws QReportingException; + void start(ExportInput exportInput, List fields) throws QReportingException; /******************************************************************************* ** Called as records flow into the pipe. @@ -48,4 +50,21 @@ public interface ReportStreamerInterface *******************************************************************************/ void finish() throws QReportingException; + /******************************************************************************* + ** + *******************************************************************************/ + default void setDisplayFormats(Map displayFormats) + { + // noop in base class + } + + /******************************************************************************* + ** + *******************************************************************************/ + default void addTotalsRow(QRecord record) throws QReportingException + { + RecordPipe recordPipe = new RecordPipe(); + recordPipe.addRecord(record); + takeRecordsFromPipe(recordPipe); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java new file mode 100644 index 00000000..fde25ff2 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java @@ -0,0 +1,273 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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; + + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; +import com.kingsrook.qqq.backend.core.exceptions.QFormulaException; +import com.kingsrook.qqq.backend.core.exceptions.QValueException; +import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class FormulaInterpreter +{ + /******************************************************************************* + ** + *******************************************************************************/ + public static Serializable interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula) throws QFormulaException + { + List results = interpretFormula(variableInterpreter, formula, new AtomicInteger(0)); + if(results.size() == 1) + { + return (results.get(0)); + } + else if(results.isEmpty()) + { + throw (new QFormulaException("No results from formula")); + } + else + { + throw (new QFormulaException("More than 1 result from formula")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula, AtomicInteger i) throws QFormulaException + { + StringBuilder functionName = new StringBuilder(); + List result = new ArrayList<>(); + + char previousChar = 0; + while(i.get() < formula.length()) + { + if(i.get() > 0) + { + previousChar = formula.charAt(i.get() - 1); + } + char c = formula.charAt(i.getAndIncrement()); + if(c == '(' && i.get() < formula.length() - 1) + { + ////////////////////////////////////////////////////////////////////////////////////////////////// + // open paren means: go into a sub-parse - to get a list of arguments for the current function // + ////////////////////////////////////////////////////////////////////////////////////////////////// + List args = interpretFormula(variableInterpreter, formula, i); + Serializable evaluate = evaluate(functionName.toString(), args, variableInterpreter); + result.add(evaluate); + } + else if(c == ')') + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // close paren means: end this sub-parse. evaluate the current function, add it to the result list, and return the result list. // + // unless we just closed a paren. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(previousChar != ')') + { + Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter); + result.add(evaluate); + } + return (result); + } + else if(c == ',') + { + ///////////////////////////////////////////////////////////////////////// + // comma means: evaluate the current thing; add it to the result list // + // unless we just closed a paren. // + ///////////////////////////////////////////////////////////////////////// + if(previousChar != ')') + { + Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter); + result.add(evaluate); + } + functionName = new StringBuilder(); + } + else + { + //////////////////////////////////////////////// + // else, we add this char to the current name // + //////////////////////////////////////////////// + functionName.append(c); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if we haven't found a result yet, assume we have just a literal, not a function call, and evaluate as such // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(result.isEmpty()) + { + if(!functionName.isEmpty()) + { + Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter); + result.add(evaluate); + } + } + + return (result); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Serializable evaluate(String functionName, List args, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException + { + // System.out.format("== Evaluating [%s](%s) ==\n", functionName, args); + switch(functionName) + { + case "ADD": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).add(numbers.get(1))); + } + case "MINUS": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).subtract(numbers.get(1))); + } + case "MULTIPLY": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).multiply(numbers.get(1))); + } + case "DIVIDE": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO)) + { + return null; + } + return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP)); + } + case "DIVIDE_SCALE": + { + List numbers = getNumberArgumentList(args, 3, variableInterpreter); + if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO)) + { + return null; + } + return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP)); + } + case "ROUND": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue()))); + } + case "SCALE": + { + List numbers = getNumberArgumentList(args, 2, variableInterpreter); + return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP)); + } + default: + { + //////////////////////////////////////////////////////////////////////////////////////// + // if there aren't arguments, then we can try to evaluate the thing not as a function // + //////////////////////////////////////////////////////////////////////////////////////// + if(CollectionUtils.nullSafeIsEmpty(args)) + { + try + { + return (ValueUtils.getValueAsBigDecimal(functionName)); + } + catch(Exception e) + { + // continue + } + + try + { + return (variableInterpreter.interpret(functionName)); + } + catch(Exception e) + { + // continue + } + } + } + } + + throw (new QFormulaException("Unable to evaluate unrecognized expression: " + functionName + "")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Serializable nullIfAnyNullArgsElse(List numbers, Supplier supplier) + { + if(numbers.stream().anyMatch(Objects::isNull)) + { + return (null); + } + return supplier.get(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static List getNumberArgumentList(List originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException + { + if(howMany != null) + { + if(!howMany.equals(originalArgs.size())) + { + throw (new QFormulaException("Wrong number of arguments (required: " + howMany + ", received: " + originalArgs.size() + ")")); + } + } + + List rs = new ArrayList<>(); + for(Serializable originalArg : originalArgs) + { + try + { + Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg)); + rs.add(ValueUtils.getValueAsBigDecimal(interpretedArg)); + } + catch(QValueException e) + { + throw (new QFormulaException("Could not process [" + originalArg + "] as a number")); + } + } + return (rs); + } + +} 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 new file mode 100644 index 00000000..6174d470 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -0,0 +1,510 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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; + + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QFormulaException; +import com.kingsrook.qqq.backend.core.exceptions.QReportingException; +import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +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.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +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.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface; +import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates; +import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates; + + +/******************************************************************************* + ** Action to generate a report!! + *******************************************************************************/ +public class GenerateReportAction +{ + ////////////////////////////////////////////////// + // viewName > PivotKey > fieldName > Aggregates // + ////////////////////////////////////////////////// + Map>>> pivotAggregates = new HashMap<>(); + + Map> totalAggregates = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void execute(ReportInput reportInput) throws QException + { + gatherData(reportInput); + output(reportInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void gatherData(ReportInput reportInput) throws QException + { + QReportMetaData report = reportInput.getInstance().getReport(reportInput.getReportName()); + QQueryFilter queryFilter = report.getQueryFilter(); + + setInputValuesInQueryFilter(reportInput, queryFilter); + + RecordPipe recordPipe = new RecordPipe(); + new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) -> + { + QueryInput queryInput = new QueryInput(reportInput.getInstance()); + queryInput.setSession(reportInput.getSession()); + queryInput.setRecordPipe(recordPipe); + queryInput.setTableName(report.getSourceTable()); + queryInput.setFilter(queryFilter); + return (new QueryAction().execute(queryInput)); + }, () -> consumeRecords(report, reportInput, recordPipe)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter) + { + if(queryFilter == null || queryFilter.getCriteria() == null) + { + return; + } + + QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter(); + variableInterpreter.addValueMap("input", reportInput.getInputValues()); + for(QFilterCriteria criterion : queryFilter.getCriteria()) + { + if(criterion.getValues() != null) + { + List newValues = new ArrayList<>(); + + for(Serializable value : criterion.getValues()) + { + String valueAsString = ValueUtils.getValueAsString(value); + Serializable interpretedValue = variableInterpreter.interpret(valueAsString); + newValues.add(interpretedValue); + } + criterion.setValues(newValues); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Integer consumeRecords(QReportMetaData report, ReportInput reportInput, RecordPipe recordPipe) + { + // todo - stream to output if report has a simple type output + List records = recordPipe.consumeAvailableRecords(); + + ////////////////////////////// + // do aggregates for pivots // + ////////////////////////////// + QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); + report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).forEach((view) -> + { + doPivotAggregates(view, table, records); + }); + + return (records.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doPivotAggregates(QReportView view, QTableMetaData table, List records) + { + Map>> viewAggregates = pivotAggregates.computeIfAbsent(view.getName(), (name) -> new HashMap<>()); + + for(QRecord record : records) + { + PivotKey key = new PivotKey(); + for(String pivotField : view.getPivotFields()) + { + key.add(pivotField, record.getValue(pivotField)); + } + + Map> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>()); + + for(QFieldMetaData field : table.getFields().values()) + { + if(field.getType().equals(QFieldType.INTEGER)) + { + @SuppressWarnings("unchecked") + AggregatesInterface fieldAggregates = (AggregatesInterface) keyAggregates.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates()); + fieldAggregates.add(record.getValueInteger(field.getName())); + + @SuppressWarnings("unchecked") + AggregatesInterface fieldTotalAggregates = (AggregatesInterface) totalAggregates.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates()); + fieldTotalAggregates.add(record.getValueInteger(field.getName())); + } + else if(field.getType().equals(QFieldType.DECIMAL)) + { + @SuppressWarnings("unchecked") + AggregatesInterface fieldAggregates = (AggregatesInterface) keyAggregates.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates()); + fieldAggregates.add(record.getValueBigDecimal(field.getName())); + + @SuppressWarnings("unchecked") + AggregatesInterface fieldTotalAggregates = (AggregatesInterface) totalAggregates.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates()); + fieldTotalAggregates.add(record.getValueBigDecimal(field.getName())); + } + // todo - more types (dates, at least?) + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void output(ReportInput reportInput) throws QReportingException, QFormulaException + { + QReportMetaData report = reportInput.getInstance().getReport(reportInput.getReportName()); + QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable()); + + List reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).toList(); + for(QReportView view : reportViews) + { + PivotOutput pivotOutput = outputPivot(reportInput, view, table); + ReportFormat reportFormat = reportInput.getReportFormat(); + + ExportInput exportInput = new ExportInput(reportInput.getInstance()); + exportInput.setSession(reportInput.getSession()); + exportInput.setReportFormat(reportFormat); + exportInput.setFilename(reportInput.getFilename()); + exportInput.setTitleRow(pivotOutput.titleRow); + exportInput.setReportOutputStream(reportInput.getReportOutputStream()); + + ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer(); + reportStreamer.setDisplayFormats(getDisplayFormatMap(view)); + reportStreamer.start(exportInput, getFields(table, view)); + + RecordPipe recordPipe = new RecordPipe(); // todo - make it an unlimited pipe or something... + recordPipe.addRecords(pivotOutput.pivotRows); + reportStreamer.takeRecordsFromPipe(recordPipe); + + if(pivotOutput.totalRow != null) + { + reportStreamer.addTotalsRow(pivotOutput.totalRow); + } + + reportStreamer.finish(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Map getDisplayFormatMap(QReportView view) + { + return (view.getColumns().stream() + .filter(c -> c.getDisplayFormat() != null) + .collect(Collectors.toMap(QReportField::getName, QReportField::getDisplayFormat))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List getFields(QTableMetaData table, QReportView view) + { + List fields = new ArrayList<>(); + for(String pivotField : view.getPivotFields()) + { + QFieldMetaData field = table.getField(pivotField); + fields.add(new QFieldMetaData(pivotField, field.getType()).withLabel(field.getLabel())); // todo do we need the type? if so need table as input here + } + for(QReportField column : view.getColumns()) + { + fields.add(new QFieldMetaData().withName(column.getName()).withLabel(column.getLabel())); // todo do we need the type? if so need table as input here + } + return (fields); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private PivotOutput outputPivot(ReportInput reportInput, QReportView view, QTableMetaData table) throws QReportingException, QFormulaException + { + QValueFormatter valueFormatter = new QValueFormatter(); + QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter(); + variableInterpreter.addValueMap("input", reportInput.getInputValues()); + variableInterpreter.addValueMap("total", getPivotValuesForInterpreter(totalAggregates)); + + /////////// + // title // + /////////// + String title = null; + if(view.getTitleFields() != null && StringUtils.hasContent(view.getTitleFormat())) + { + List titleValues = new ArrayList<>(); + for(String titleField : view.getTitleFields()) + { + titleValues.add(variableInterpreter.interpret(titleField)); + } + + title = valueFormatter.formatStringWithValues(view.getTitleFormat(), titleValues); + } + else if(StringUtils.hasContent(view.getTitleFormat())) + { + title = view.getTitleFormat(); + } + + if(StringUtils.hasContent(title)) + { + System.out.println(title); + } + + ///////////// + // headers // + ///////////// + for(String field : view.getPivotFields()) + { + System.out.printf("%-15s", table.getField(field).getLabel()); + } + + for(QReportField column : view.getColumns()) + { + System.out.printf("%25s", column.getLabel()); + } + System.out.println(); + + /////////////////////// + // create pivot rows // + /////////////////////// + List pivotRows = new ArrayList<>(); + for(Map.Entry>> entry : pivotAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet()) + { + PivotKey pivotKey = entry.getKey(); + Map> fieldAggregates = entry.getValue(); + variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates)); + + QRecord pivotRow = new QRecord(); + pivotRows.add(pivotRow); + for(Pair key : pivotKey.getKeys()) + { + pivotRow.setValue(key.getA(), key.getB()); + } + + for(QReportField column : view.getColumns()) + { + Serializable serializable = getValueForColumn(variableInterpreter, column); + pivotRow.setValue(column.getName(), serializable); + } + } + + ///////////////////////// + // sort the pivot rows // + ///////////////////////// + if(CollectionUtils.nullSafeHasContents(view.getOrderByFields())) + { + pivotRows.sort((o1, o2) -> + { + return pivotRowComparator(view, o1, o2); + }); + } + + ///////////////////////////////////////////// + // print the rows (just debugging i think) // + ///////////////////////////////////////////// + for(QRecord pivotRow : pivotRows) + { + for(String pivotField : view.getPivotFields()) + { + System.out.printf("%-15s", pivotRow.getValue(pivotField)); + } + + for(QReportField column : view.getColumns()) + { + Serializable serializable = pivotRow.getValue(column.getName()); + String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable); + System.out.printf("%25s", formatted); + } + + System.out.println(); + } + + //////////////// + // totals row // + //////////////// + QRecord totalRow = null; + if(view.getTotalRow()) + { + totalRow = new QRecord(); + + for(String pivotField : view.getPivotFields()) + { + if(totalRow.getValues().isEmpty()) + { + totalRow.setValue(pivotField, "Totals"); + System.out.printf("%-15s", "Totals"); + } + else + { + System.out.printf("%-15s", ""); + } + } + + variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates)); + for(QReportField column : view.getColumns()) + { + Serializable serializable = getValueForColumn(variableInterpreter, column); + totalRow.setValue(column.getName(), serializable); + + String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable); + System.out.printf("%25s", formatted); + } + + System.out.println(); + } + + return (new PivotOutput(pivotRows, title, totalRow)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Serializable getValueForColumn(QMetaDataVariableInterpreter variableInterpreter, QReportField column) throws QFormulaException + { + String formula = column.getFormula(); + Serializable serializable = variableInterpreter.interpretForObject(formula); + if(formula.startsWith("=") && formula.length() > 1) + { + // serializable = interpretFormula(variableInterpreter, formula); + serializable = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1)); + } + return serializable; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings({ "rawtypes", "unchecked" }) + private int pivotRowComparator(QReportView view, QRecord o1, QRecord o2) + { + if(o1 == o2) + { + return (0); + } + + for(QFilterOrderBy orderByField : view.getOrderByFields()) + { + Comparable c1 = (Comparable) o1.getValue(orderByField.getFieldName()); + Comparable c2 = (Comparable) o2.getValue(orderByField.getFieldName()); + + if(c1 == null && c2 == null) + { + continue; + } + if(c1 == null) + { + return (orderByField.getIsAscending() ? -1 : 1); + } + if(c2 == null) + { + return (orderByField.getIsAscending() ? 1 : -1); + } + + int comp = orderByField.getIsAscending() ? c1.compareTo(c2) : c2.compareTo(c1); + if(comp != 0) + { + return (comp); + } + } + + return (0); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private Map getPivotValuesForInterpreter(Map> fieldAggregates) + { + Map pivotValuesForInterpreter = new HashMap<>(); + for(Map.Entry> subEntry : fieldAggregates.entrySet()) + { + String fieldName = subEntry.getKey(); + AggregatesInterface aggregates = subEntry.getValue(); + pivotValuesForInterpreter.put("sum." + fieldName, aggregates.getSum()); + pivotValuesForInterpreter.put("count." + fieldName, aggregates.getCount()); + // todo min, max, avg + } + return pivotValuesForInterpreter; + } + + + + /******************************************************************************* + ** record to serve as tuple/multi-value output of outputPivot method. + *******************************************************************************/ + private record PivotOutput(List pivotRows, String titleRow, QRecord totalRow) + { + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java new file mode 100644 index 00000000..c844dbbb --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java @@ -0,0 +1,145 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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; + + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QReportingException; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Report streamer implementation that just builds up a STATIC list of lists of strings. + ** Meant only for use in unit tests at this time... would need refactored for + ** multi-thread/multi-use if wanted for real usage. + *******************************************************************************/ +public class ListOfMapsExportStreamer implements ExportStreamerInterface +{ + private static final Logger LOG = LogManager.getLogger(ListOfMapsExportStreamer.class); + + private ExportInput exportInput; + private List fields; + + private static List> list = new ArrayList<>(); + private static List headers = new ArrayList<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ListOfMapsExportStreamer() + { + } + + + + /******************************************************************************* + ** Getter for list + ** + *******************************************************************************/ + public static List> getList() + { + return (list); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void start(ExportInput exportInput, List fields) throws QReportingException + { + this.exportInput = exportInput; + this.fields = fields; + + headers = new ArrayList<>(); + for(QFieldMetaData field : fields) + { + headers.add(field.getLabel()); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException + { + List qRecords = recordPipe.consumeAvailableRecords(); + LOG.info("Consuming [" + qRecords.size() + "] records from the pipe"); + + for(QRecord qRecord : qRecords) + { + addRecord(qRecord); + } + return (qRecords.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void addRecord(QRecord qRecord) + { + Map row = new LinkedHashMap<>(); + list.add(row); + for(int i = 0; i < fields.size(); i++) + { + row.put(headers.get(i), qRecord.getValueString(fields.get(i).getName())); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void addTotalsRow(QRecord record) throws QReportingException + { + addRecord(record); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void finish() + { + + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java new file mode 100644 index 00000000..6d926a41 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java @@ -0,0 +1,111 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.utils.Pair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class PivotKey +{ + private List> keys = new ArrayList<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public PivotKey() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return "PivotKey{keys=" + keys + '}'; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void add(String field, Serializable value) + { + keys.add(new Pair<>(field, value)); + } + + + + /******************************************************************************* + ** Getter for keys + ** + *******************************************************************************/ + public List> getKeys() + { + return keys; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + if(o == null || getClass() != o.getClass()) + { + return false; + } + PivotKey pivotKey = (PivotKey) o; + return Objects.equals(keys, pivotKey.keys); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int hashCode() + { + return Objects.hash(keys); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 3af8b35d..2b96e203 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.io.Serializable; import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -47,6 +48,26 @@ public class QValueFormatter ** *******************************************************************************/ public String formatValue(QFieldMetaData field, Serializable value) + { + return (formatValue(field.getDisplayFormat(), field.getName(), value)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String formatValue(String displayFormat, Serializable value) + { + return (formatValue(displayFormat, "", value)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String formatValue(String displayFormat, String fieldName, Serializable value) { ////////////////////////////////// // null values get null results // @@ -59,11 +80,11 @@ public class QValueFormatter //////////////////////////////////////////////////////// // if the field has a display format, try to apply it // //////////////////////////////////////////////////////// - if(StringUtils.hasContent(field.getDisplayFormat())) + if(StringUtils.hasContent(displayFormat)) { try { - return (field.getDisplayFormat().formatted(value)); + return (displayFormat.formatted(value)); } catch(Exception e) { @@ -72,24 +93,24 @@ public class QValueFormatter // todo - revisit if we actually want this - or - if you should get an error if you mis-configure your table this way (ideally during validation!) if(e.getMessage().equals("f != java.lang.Integer")) { - return formatValue(field, ValueUtils.getValueAsBigDecimal(value)); + return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value)); } else if(e.getMessage().equals("f != java.lang.String")) { - return formatValue(field, ValueUtils.getValueAsBigDecimal(value)); + return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value)); } else if(e.getMessage().equals("d != java.math.BigDecimal")) { - return formatValue(field, ValueUtils.getValueAsInteger(value)); + return formatValue(displayFormat, ValueUtils.getValueAsInteger(value)); } else { - LOG.warn("Error formatting value [" + value + "] for field [" + field.getName() + "] with format [" + field.getDisplayFormat() + "]: " + e.getMessage()); + LOG.warn("Error formatting value [" + value + "] for field [" + fieldName + "] with format [" + displayFormat + "]: " + e.getMessage()); } } catch(Exception e2) { - LOG.warn("Caught secondary exception trying to convert type on field [" + field.getName() + "] for formatting", e); + LOG.warn("Caught secondary exception trying to convert type on field [" + fieldName + "] for formatting", e); } } } @@ -117,11 +138,7 @@ public class QValueFormatter /////////////////////////////////////////////////////////////////////// try { - List values = table.getRecordLabelFields().stream() - .map(record::getValue) - .map(v -> v == null ? "" : v) - .toList(); - return (table.getRecordLabelFormat().formatted(values.toArray())); + return formatStringWithFields(table.getRecordLabelFormat(), table.getRecordLabelFields(), record.getValues()); } catch(Exception e) { @@ -131,6 +148,33 @@ public class QValueFormatter + /******************************************************************************* + ** + *******************************************************************************/ + public String formatStringWithFields(String formatString, List formatFields, Map valueMap) + { + List values = formatFields.stream() + .map(valueMap::get) + .map(v -> v == null ? "" : v) + .toList(); + return (formatString.formatted(values.toArray())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String formatStringWithValues(String formatString, List formatValues) + { + List values = formatValues.stream() + .map(v -> v == null ? "" : v) + .toList(); + return (formatString.formatted(values.toArray())); + } + + + /******************************************************************************* ** Deal with non-happy-path cases for making a record label. *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QFormulaException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QFormulaException.java new file mode 100644 index 00000000..62356d09 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QFormulaException.java @@ -0,0 +1,51 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.exceptions; + + +/******************************************************************************* + * Exception thrown while generating reports + * + *******************************************************************************/ +public class QFormulaException extends QException +{ + + /******************************************************************************* + ** Constructor of message + ** + *******************************************************************************/ + public QFormulaException(String message) + { + super(message); + } + + + + /******************************************************************************* + ** Constructor of message & cause + ** + *******************************************************************************/ + public QFormulaException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java index 7549d047..c52009e4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java @@ -22,11 +22,14 @@ package com.kingsrook.qqq.backend.core.instances; +import java.io.Serializable; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvEntry; import org.apache.logging.log4j.LogManager; @@ -48,7 +51,8 @@ public class QMetaDataVariableInterpreter { private static final Logger LOG = LogManager.getLogger(QMetaDataVariableInterpreter.class); - private Map environmentOverrides; + private Map environmentOverrides; + private Map> valueMaps; @@ -121,6 +125,18 @@ public class QMetaDataVariableInterpreter + /******************************************************************************* + ** Interpret a value string, which may be a variable, into its run-time value - + ** always as a String. + ** + *******************************************************************************/ + public String interpret(String value) + { + return (ValueUtils.getValueAsString(interpretForObject(value))); + } + + + /******************************************************************************* ** Interpret a value string, which may be a variable, into its run-time value. ** @@ -131,7 +147,7 @@ public class QMetaDataVariableInterpreter ** - used if you really want to get back the literal value, ${env.X}, for example. ** Else the output is the input. *******************************************************************************/ - public String interpret(String value) + public Serializable interpretForObject(String value) { if(value == null) { @@ -142,23 +158,39 @@ public class QMetaDataVariableInterpreter if(value.startsWith(envPrefix) && value.endsWith("}")) { String envVarName = value.substring(envPrefix.length()).replaceFirst("}$", ""); - String envValue = getEnvironmentVariable(envVarName); - return (envValue); + return (getEnvironmentVariable(envVarName)); } String propPrefix = "${prop."; if(value.startsWith(propPrefix) && value.endsWith("}")) { - String propertyName = value.substring(propPrefix.length()).replaceFirst("}$", ""); - String propertyValue = System.getProperty(propertyName); - return (propertyValue); + String propertyName = value.substring(propPrefix.length()).replaceFirst("}$", ""); + return (System.getProperty(propertyName)); } String literalPrefix = "${literal."; if(value.startsWith(literalPrefix) && value.endsWith("}")) { - String literalValue = value.substring(literalPrefix.length()).replaceFirst("}$", ""); - return (literalValue); + return (value.substring(literalPrefix.length()).replaceFirst("}$", "")); + } + + if(valueMaps != null) + { + for(Map.Entry> entry : valueMaps.entrySet()) + { + String name = entry.getKey(); + Map valueMap = entry.getValue(); + + String prefix = "${" + name + "."; + if(value.startsWith(prefix) && value.endsWith("}")) + { + String lookupName = value.substring(prefix.length()).replaceFirst("}$", ""); + if(valueMap != null && valueMap.containsKey(lookupName)) + { + return (valueMap.get(lookupName)); + } + } + } } return (value); @@ -190,4 +222,19 @@ public class QMetaDataVariableInterpreter return System.getenv(key); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addValueMap(String name, Map values) + { + if(valueMaps == null) + { + valueMaps = new LinkedHashMap<>(); + } + + valueMaps.put(name, values); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ExportInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ExportInput.java new file mode 100644 index 00000000..0e8dbde8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ExportInput.java @@ -0,0 +1,228 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.model.actions.reporting; + + +import java.io.OutputStream; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +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.session.QSession; + + +/******************************************************************************* + ** Input for an Export action + *******************************************************************************/ +public class ExportInput extends AbstractTableActionInput +{ + private QQueryFilter queryFilter; + private Integer limit; + private List fieldNames; + + private String filename; + private ReportFormat reportFormat; + private OutputStream reportOutputStream; + private String titleRow; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ExportInput() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ExportInput(QInstance instance) + { + super(instance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public ExportInput(QInstance instance, QSession session) + { + super(instance); + setSession(session); + } + + + + /******************************************************************************* + ** Getter for queryFilter + ** + *******************************************************************************/ + public QQueryFilter getQueryFilter() + { + return queryFilter; + } + + + + /******************************************************************************* + ** Setter for queryFilter + ** + *******************************************************************************/ + public void setQueryFilter(QQueryFilter queryFilter) + { + this.queryFilter = queryFilter; + } + + + + /******************************************************************************* + ** Getter for limit + ** + *******************************************************************************/ + public Integer getLimit() + { + return limit; + } + + + + /******************************************************************************* + ** Setter for limit + ** + *******************************************************************************/ + public void setLimit(Integer limit) + { + this.limit = limit; + } + + + + /******************************************************************************* + ** Getter for fieldNames + ** + *******************************************************************************/ + public List getFieldNames() + { + return fieldNames; + } + + + + /******************************************************************************* + ** Setter for fieldNames + ** + *******************************************************************************/ + public void setFieldNames(List fieldNames) + { + this.fieldNames = fieldNames; + } + + + + /******************************************************************************* + ** Getter for filename + ** + *******************************************************************************/ + public String getFilename() + { + return filename; + } + + + + /******************************************************************************* + ** Setter for filename + ** + *******************************************************************************/ + public void setFilename(String filename) + { + this.filename = filename; + } + + + + /******************************************************************************* + ** Getter for reportFormat + ** + *******************************************************************************/ + public ReportFormat getReportFormat() + { + return reportFormat; + } + + + + /******************************************************************************* + ** Setter for reportFormat + ** + *******************************************************************************/ + public void setReportFormat(ReportFormat reportFormat) + { + this.reportFormat = reportFormat; + } + + + + /******************************************************************************* + ** Getter for reportOutputStream + ** + *******************************************************************************/ + public OutputStream getReportOutputStream() + { + return reportOutputStream; + } + + + + /******************************************************************************* + ** Setter for reportOutputStream + ** + *******************************************************************************/ + public void setReportOutputStream(OutputStream reportOutputStream) + { + this.reportOutputStream = reportOutputStream; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public String getTitleRow() + { + return titleRow; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setTitleRow(String titleRow) + { + this.titleRow = titleRow; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ExportOutput.java similarity index 95% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportOutput.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ExportOutput.java index 01e3e0de..b88e7967 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ExportOutput.java @@ -26,9 +26,9 @@ import java.io.Serializable; /******************************************************************************* - ** Output for a Report action + ** Output for an Export action *******************************************************************************/ -public class ReportOutput implements Serializable +public class ExportOutput implements Serializable { public long recordCount; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportFormat.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportFormat.java index 045229a4..9b41121d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportFormat.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportFormat.java @@ -24,9 +24,10 @@ package com.kingsrook.qqq.backend.core.model.actions.reporting; import java.util.Locale; import java.util.function.Supplier; -import com.kingsrook.qqq.backend.core.actions.reporting.CsvReportStreamer; -import com.kingsrook.qqq.backend.core.actions.reporting.ExcelReportStreamer; -import com.kingsrook.qqq.backend.core.actions.reporting.ReportStreamerInterface; +import com.kingsrook.qqq.backend.core.actions.reporting.CsvExportStreamer; +import com.kingsrook.qqq.backend.core.actions.reporting.ExcelExportStreamer; +import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface; +import com.kingsrook.qqq.backend.core.actions.reporting.ListOfMapsExportStreamer; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.dhatim.fastexcel.Worksheet; @@ -37,22 +38,23 @@ import org.dhatim.fastexcel.Worksheet; *******************************************************************************/ public enum ReportFormat { - XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelReportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), - CSV(null, null, CsvReportStreamer::new, "text/csv"); + XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + CSV(null, null, CsvExportStreamer::new, "text/csv"), + LIST_OF_MAPS(null, null, ListOfMapsExportStreamer::new, null); private final Integer maxRows; private final Integer maxCols; private final String mimeType; - private final Supplier streamerConstructor; + private final Supplier streamerConstructor; /******************************************************************************* ** *******************************************************************************/ - ReportFormat(Integer maxRows, Integer maxCols, Supplier streamerConstructor, String mimeType) + ReportFormat(Integer maxRows, Integer maxCols, Supplier streamerConstructor, String mimeType) { this.maxRows = maxRows; this.maxCols = maxCols; @@ -94,6 +96,7 @@ public enum ReportFormat } + /******************************************************************************* ** Getter for maxCols ** @@ -119,7 +122,7 @@ public enum ReportFormat /******************************************************************************* ** *******************************************************************************/ - public ReportStreamerInterface newReportStreamer() + public ExportStreamerInterface newReportStreamer() { return (streamerConstructor.get()); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportInput.java index 211799d2..d86bb5dd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportInput.java @@ -23,21 +23,20 @@ package com.kingsrook.qqq.backend.core.model.actions.reporting; import java.io.OutputStream; -import java.util.List; +import java.io.Serializable; +import java.util.Map; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; -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.session.QSession; /******************************************************************************* - ** Input for a Report action + ** Input for an Export action *******************************************************************************/ public class ReportInput extends AbstractTableActionInput { - private QQueryFilter queryFilter; - private Integer limit; - private List fieldNames; + private String reportName; + private Map inputValues; private String filename; private ReportFormat reportFormat; @@ -76,67 +75,45 @@ public class ReportInput extends AbstractTableActionInput /******************************************************************************* - ** Getter for queryFilter + ** Getter for reportName ** *******************************************************************************/ - public QQueryFilter getQueryFilter() + public String getReportName() { - return queryFilter; + return reportName; } /******************************************************************************* - ** Setter for queryFilter + ** Setter for reportName ** *******************************************************************************/ - public void setQueryFilter(QQueryFilter queryFilter) + public void setReportName(String reportName) { - this.queryFilter = queryFilter; + this.reportName = reportName; } /******************************************************************************* - ** Getter for limit + ** Getter for inputValues ** *******************************************************************************/ - public Integer getLimit() + public Map getInputValues() { - return limit; + return inputValues; } /******************************************************************************* - ** Setter for limit + ** Setter for inputValues ** *******************************************************************************/ - public void setLimit(Integer limit) + public void setInputValues(Map inputValues) { - this.limit = limit; - } - - - - /******************************************************************************* - ** Getter for fieldNames - ** - *******************************************************************************/ - public List getFieldNames() - { - return fieldNames; - } - - - - /******************************************************************************* - ** Setter for fieldNames - ** - *******************************************************************************/ - public void setFieldNames(List fieldNames) - { - this.fieldNames = fieldNames; + this.inputValues = inputValues; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java index 1c6933af..51118b38 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java @@ -31,11 +31,42 @@ import java.io.Serializable; *******************************************************************************/ public class QFilterOrderBy implements Serializable { - private String fieldName; + private String fieldName; private boolean isAscending = true; + /******************************************************************************* + ** Default no-arg constructor + *******************************************************************************/ + public QFilterOrderBy() + { + + } + + + + /******************************************************************************* + ** Constructor that sets field name, but leaves default for isAscending (true) + *******************************************************************************/ + public QFilterOrderBy(String fieldName) + { + this.fieldName = fieldName; + } + + + + /******************************************************************************* + ** Constructor that takes field name and isAscending. + *******************************************************************************/ + public QFilterOrderBy(String fieldName, boolean isAscending) + { + this.fieldName = fieldName; + this.isAscending = isAscending; + } + + + /******************************************************************************* ** Getter for fieldName ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 1fd65d4c..8f81d232 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -64,6 +65,7 @@ public class QInstance private Map possibleValueSources = new LinkedHashMap<>(); private Map processes = new LinkedHashMap<>(); private Map apps = new LinkedHashMap<>(); + private Map reports = new LinkedHashMap<>(); private Map widgets = new LinkedHashMap<>(); @@ -432,6 +434,66 @@ public class QInstance + /******************************************************************************* + ** + *******************************************************************************/ + public void addReport(QReportMetaData report) + { + this.addReport(report.getName(), report); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addReport(String name, QReportMetaData report) + { + if(!StringUtils.hasContent(name)) + { + throw (new IllegalArgumentException("Attempted to add an report without a name.")); + } + if(this.reports.containsKey(name)) + { + throw (new IllegalArgumentException("Attempted to add a second report with name: " + name)); + } + this.reports.put(name, report); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QReportMetaData getReport(String name) + { + return (this.reports.get(name)); + } + + + + /******************************************************************************* + ** Getter for reports + ** + *******************************************************************************/ + public Map getReports() + { + return reports; + } + + + + /******************************************************************************* + ** Setter for reports + ** + *******************************************************************************/ + public void setReports(Map reports) + { + this.reports = reports; + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DisplayFormat.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DisplayFormat.java index 4c884ed0..eed23a56 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DisplayFormat.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DisplayFormat.java @@ -28,13 +28,51 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields; public interface DisplayFormat { String DEFAULT = "%s"; - String STRING = "%s"; - String COMMAS = "%,d"; + String STRING = "%s"; + String COMMAS = "%,d"; + String DECIMAL1_COMMAS = "%,.1f"; String DECIMAL2_COMMAS = "%,.2f"; String DECIMAL3_COMMAS = "%,.3f"; + String DECIMAL1 = "%.1f"; String DECIMAL2 = "%.2f"; String DECIMAL3 = "%.3f"; + String CURRENCY = "$%,.2f"; + + String PERCENT = "%.0f%%"; + String PERCENT_POINT1 = "%.1f%%"; + String PERCENT_POINT2 = "%.2f%%"; + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:Indentation") + static String getExcelFormat(String javaDisplayFormat) + { + if(javaDisplayFormat == null) + { + return (null); + } + + return switch(javaDisplayFormat) + { + case DisplayFormat.DEFAULT -> null; + case DisplayFormat.COMMAS -> "#,##0"; + case DisplayFormat.DECIMAL1 -> "0.0"; + case DisplayFormat.DECIMAL2 -> "0.00"; + case DisplayFormat.DECIMAL3 -> "0.000"; + case DisplayFormat.DECIMAL1_COMMAS -> "#,##0.0"; + case DisplayFormat.DECIMAL2_COMMAS -> "#,##0.00"; + case DisplayFormat.DECIMAL3_COMMAS -> "#,##0.000"; + case DisplayFormat.CURRENCY -> "$#,##0.00"; + case DisplayFormat.PERCENT -> "0%"; + case DisplayFormat.PERCENT_POINT1 -> "0.0%"; + case DisplayFormat.PERCENT_POINT2 -> "0.00%"; + default -> null; + }; + + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java new file mode 100644 index 00000000..38a1aedd --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java @@ -0,0 +1,207 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.model.metadata.reporting; + + +/******************************************************************************* + ** Field within a report + *******************************************************************************/ +public class QReportField +{ + private String name; + private String label; + private String fieldName; + private String formula; + private String displayFormat; + // todo - type? + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public QReportField withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public QReportField withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for fieldName + ** + *******************************************************************************/ + public String getFieldName() + { + return fieldName; + } + + + + /******************************************************************************* + ** Setter for fieldName + ** + *******************************************************************************/ + public void setFieldName(String fieldName) + { + this.fieldName = fieldName; + } + + + + /******************************************************************************* + ** Fluent setter for fieldName + ** + *******************************************************************************/ + public QReportField withFieldName(String fieldName) + { + this.fieldName = fieldName; + return (this); + } + + + + /******************************************************************************* + ** Getter for formula + ** + *******************************************************************************/ + public String getFormula() + { + return formula; + } + + + + /******************************************************************************* + ** Setter for formula + ** + *******************************************************************************/ + public void setFormula(String formula) + { + this.formula = formula; + } + + + + /******************************************************************************* + ** Fluent setter for formula + ** + *******************************************************************************/ + public QReportField withFormula(String formula) + { + this.formula = formula; + return (this); + } + + + + /******************************************************************************* + ** Getter for displayFormat + ** + *******************************************************************************/ + public String getDisplayFormat() + { + return displayFormat; + } + + + + /******************************************************************************* + ** Setter for displayFormat + ** + *******************************************************************************/ + public void setDisplayFormat(String displayFormat) + { + this.displayFormat = displayFormat; + } + + + + /******************************************************************************* + ** Fluent setter for displayFormat + ** + *******************************************************************************/ + public QReportField withDisplayFormat(String displayFormat) + { + this.displayFormat = displayFormat; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java new file mode 100644 index 00000000..9854a8e8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java @@ -0,0 +1,246 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.model.metadata.reporting; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; + + +/******************************************************************************* + ** Meta-data definition of a report generated by QQQ + *******************************************************************************/ +public class QReportMetaData +{ + private String name; + private String label; + private List inputFields; + private String sourceTable; + private QQueryFilter queryFilter; + private List views; + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public QReportMetaData withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public QReportMetaData withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for inputFields + ** + *******************************************************************************/ + public List getInputFields() + { + return inputFields; + } + + + + /******************************************************************************* + ** Setter for inputFields + ** + *******************************************************************************/ + public void setInputFields(List inputFields) + { + this.inputFields = inputFields; + } + + + + /******************************************************************************* + ** Fluent setter for inputFields + ** + *******************************************************************************/ + public QReportMetaData withInputFields(List inputFields) + { + this.inputFields = inputFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for sourceTable + ** + *******************************************************************************/ + public String getSourceTable() + { + return sourceTable; + } + + + + /******************************************************************************* + ** Setter for sourceTable + ** + *******************************************************************************/ + public void setSourceTable(String sourceTable) + { + this.sourceTable = sourceTable; + } + + + + /******************************************************************************* + ** Fluent setter for sourceTable + ** + *******************************************************************************/ + public QReportMetaData withSourceTable(String sourceTable) + { + this.sourceTable = sourceTable; + return (this); + } + + + + /******************************************************************************* + ** Getter for queryFilter + ** + *******************************************************************************/ + public QQueryFilter getQueryFilter() + { + return queryFilter; + } + + + + /******************************************************************************* + ** Setter for queryFilter + ** + *******************************************************************************/ + public void setQueryFilter(QQueryFilter queryFilter) + { + this.queryFilter = queryFilter; + } + + + + /******************************************************************************* + ** Fluent setter for queryFilter + ** + *******************************************************************************/ + public QReportMetaData withQueryFilter(QQueryFilter queryFilter) + { + this.queryFilter = queryFilter; + return (this); + } + + + + /******************************************************************************* + ** Getter for views + ** + *******************************************************************************/ + public List getViews() + { + return views; + } + + + + /******************************************************************************* + ** Setter for views + ** + *******************************************************************************/ + public void setViews(List views) + { + this.views = views; + } + + + + /******************************************************************************* + ** Fluent setter for views + ** + *******************************************************************************/ + public QReportMetaData withViews(List views) + { + this.views = views; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java new file mode 100644 index 00000000..4e2054ed --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java @@ -0,0 +1,350 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.model.metadata.reporting; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QReportView +{ + private String name; + private String label; + private ReportType type; + private String titleFormat; + private List titleFields; + private List pivotFields; + private boolean totalRow = false; + private List columns; + private List orderByFields; + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public QReportView withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public QReportView withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public ReportType getType() + { + return type; + } + + + + /******************************************************************************* + ** Setter for type + ** + *******************************************************************************/ + public void setType(ReportType type) + { + this.type = type; + } + + + + /******************************************************************************* + ** Fluent setter for type + ** + *******************************************************************************/ + public QReportView withType(ReportType type) + { + this.type = type; + return (this); + } + + + + /******************************************************************************* + ** Getter for titleFormat + ** + *******************************************************************************/ + public String getTitleFormat() + { + return titleFormat; + } + + + + /******************************************************************************* + ** Setter for titleFormat + ** + *******************************************************************************/ + public void setTitleFormat(String titleFormat) + { + this.titleFormat = titleFormat; + } + + + + /******************************************************************************* + ** Fluent setter for titleFormat + ** + *******************************************************************************/ + public QReportView withTitleFormat(String titleFormat) + { + this.titleFormat = titleFormat; + return (this); + } + + + + /******************************************************************************* + ** Getter for titleFields + ** + *******************************************************************************/ + public List getTitleFields() + { + return titleFields; + } + + + + /******************************************************************************* + ** Setter for titleFields + ** + *******************************************************************************/ + public void setTitleFields(List titleFields) + { + this.titleFields = titleFields; + } + + + + /******************************************************************************* + ** Fluent setter for titleFields + ** + *******************************************************************************/ + public QReportView withTitleFields(List titleFields) + { + this.titleFields = titleFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for pivotFields + ** + *******************************************************************************/ + public List getPivotFields() + { + return pivotFields; + } + + + + /******************************************************************************* + ** Setter for pivotFields + ** + *******************************************************************************/ + public void setPivotFields(List pivotFields) + { + this.pivotFields = pivotFields; + } + + + + /******************************************************************************* + ** Fluent setter for pivotFields + ** + *******************************************************************************/ + public QReportView withPivotFields(List pivotFields) + { + this.pivotFields = pivotFields; + return (this); + } + + + + /******************************************************************************* + ** Getter for totalRow + ** + *******************************************************************************/ + public boolean getTotalRow() + { + return totalRow; + } + + + + /******************************************************************************* + ** Setter for totalRow + ** + *******************************************************************************/ + public void setTotalRow(boolean totalRow) + { + this.totalRow = totalRow; + } + + + + /******************************************************************************* + ** Fluent setter for totalRow + ** + *******************************************************************************/ + public QReportView withTotalRow(boolean totalRow) + { + this.totalRow = totalRow; + return (this); + } + + + + /******************************************************************************* + ** Getter for columns + ** + *******************************************************************************/ + public List getColumns() + { + return columns; + } + + + + /******************************************************************************* + ** Setter for columns + ** + *******************************************************************************/ + public void setColumns(List columns) + { + this.columns = columns; + } + + + + /******************************************************************************* + ** Fluent setter for columns + ** + *******************************************************************************/ + public QReportView withColumns(List columns) + { + this.columns = columns; + return (this); + } + + + + /******************************************************************************* + ** Getter for orderByFields + ** + *******************************************************************************/ + public List getOrderByFields() + { + return orderByFields; + } + + + + /******************************************************************************* + ** Setter for orderByFields + ** + *******************************************************************************/ + public void setOrderByFields(List orderByFields) + { + this.orderByFields = orderByFields; + } + + + + /******************************************************************************* + ** Fluent setter for orderByFields + ** + *******************************************************************************/ + public QReportView withOrderByFields(List orderByFields) + { + this.orderByFields = orderByFields; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java new file mode 100644 index 00000000..f3f2f243 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/ReportType.java @@ -0,0 +1,32 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.model.metadata.reporting; + + +/******************************************************************************* + ** Types of reports that QQQ can generate + *******************************************************************************/ +public enum ReportType +{ + PIVOT, + SIMPLE +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index 8dbd9d07..a7447453 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.commons.lang.NotImplementedException; @@ -172,6 +173,16 @@ public class MemoryRecordStore recordMatches = !testIn(criterion, value); break; } + case IS_BLANK: + { + recordMatches = testBlank(criterion, value); + break; + } + case IS_NOT_BLANK: + { + recordMatches = !testBlank(criterion, value); + break; + } case CONTAINS: { recordMatches = testContains(criterion, fieldName, value); @@ -182,6 +193,26 @@ public class MemoryRecordStore recordMatches = !testContains(criterion, fieldName, value); break; } + case STARTS_WITH: + { + recordMatches = testStartsWith(criterion, fieldName, value); + break; + } + case NOT_STARTS_WITH: + { + recordMatches = !testStartsWith(criterion, fieldName, value); + break; + } + case ENDS_WITH: + { + recordMatches = testEndsWith(criterion, fieldName, value); + break; + } + case NOT_ENDS_WITH: + { + recordMatches = !testEndsWith(criterion, fieldName, value); + break; + } case GREATER_THAN: { recordMatches = testGreaterThan(criterion, value); @@ -202,6 +233,22 @@ public class MemoryRecordStore recordMatches = !testGreaterThan(criterion, value); break; } + case BETWEEN: + { + QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues()); + QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues())); + criteria1.getValues().remove(0); + recordMatches = (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); + break; + } + case NOT_BETWEEN: + { + QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues()); + QFilterCriteria criteria1 = new QFilterCriteria().withValues(criterion.getValues()); + criteria1.getValues().remove(0); + recordMatches = !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value)); + break; + } default: { throw new NotImplementedException("Operator [" + criterion.getOperator() + "] is not yet implemented in the Memory backend."); @@ -218,6 +265,26 @@ public class MemoryRecordStore + /******************************************************************************* + ** + *******************************************************************************/ + private boolean testBlank(QFilterCriteria criterion, Serializable value) + { + if(value == null) + { + return (true); + } + + if("".equals(ValueUtils.getValueAsString(value))) + { + return (true); + } + + return (false); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -229,7 +296,7 @@ public class MemoryRecordStore throw (new IllegalArgumentException("Missing criterion value in query")); } - if (value == null) + if(value == null) { ///////////////////////////////////////////////////////////////////////////////////// // a database would say 'false' for if a null column is > a value, so do the same. // @@ -247,6 +314,31 @@ public class MemoryRecordStore return (valueNumber.doubleValue() > criterionValueNumber.doubleValue()); } + if(value instanceof LocalDate || criterionValue instanceof LocalDate) + { + LocalDate valueDate; + if(value instanceof LocalDate ld) + { + valueDate = ld; + } + else + { + valueDate = ValueUtils.getValueAsLocalDate(value); + } + + LocalDate criterionDate; + if(criterionValue instanceof LocalDate ld) + { + criterionDate = ld; + } + else + { + criterionDate = ValueUtils.getValueAsLocalDate(criterionValue); + } + + return (valueDate.isAfter(criterionDate)); + } + throw (new NotImplementedException("Greater/Less Than comparisons are not (yet?) implemented for the supplied types [" + value.getClass().getSimpleName() + "][" + criterionValue.getClass().getSimpleName() + "]")); } @@ -303,6 +395,42 @@ public class MemoryRecordStore + /******************************************************************************* + ** + *******************************************************************************/ + private boolean testStartsWith(QFilterCriteria criterion, String fieldName, Serializable value) + { + String stringValue = getStringFieldValue(value, fieldName, criterion); + String criterionValue = getFirstStringCriterionValue(criterion); + + if(!stringValue.startsWith(criterionValue)) + { + return (false); + } + + return (true); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private boolean testEndsWith(QFilterCriteria criterion, String fieldName, Serializable value) + { + String stringValue = getStringFieldValue(value, fieldName, criterion); + String criterionValue = getFirstStringCriterionValue(criterion); + + if(!stringValue.endsWith(criterionValue)) + { + return (false); + } + + return (true); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Pair.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Pair.java new file mode 100644 index 00000000..4e76dbfa --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/Pair.java @@ -0,0 +1,107 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.utils; + + +import java.util.Objects; + + +/******************************************************************************* + ** Simple container for two objects + *******************************************************************************/ +public class Pair +{ + private A a; + private B b; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Pair(A a, B b) + { + this.a = a; + this.b = b; + } + + + + @Override + public String toString() + { + return (a + ":" + b); + } + + + + /******************************************************************************* + ** Getter for a + ** + *******************************************************************************/ + public A getA() + { + return a; + } + + + + /******************************************************************************* + ** Getter for b + ** + *******************************************************************************/ + public B getB() + { + return b; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + if(o == null || getClass() != o.getClass()) + { + return false; + } + Pair pair = (Pair) o; + return Objects.equals(a, pair.a) && Objects.equals(b, pair.b); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int hashCode() + { + return Objects.hash(a, b); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesInterface.java new file mode 100644 index 00000000..9bfe8d0a --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesInterface.java @@ -0,0 +1,63 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.utils.aggregates; + + +import java.io.Serializable; +import java.math.BigDecimal; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface AggregatesInterface +{ + /******************************************************************************* + ** + *******************************************************************************/ + void add(T t); + + /******************************************************************************* + ** + *******************************************************************************/ + int getCount(); + + /******************************************************************************* + ** + *******************************************************************************/ + T getSum(); + + /******************************************************************************* + ** + *******************************************************************************/ + T getMin(); + + /******************************************************************************* + ** + *******************************************************************************/ + T getMax(); + + /******************************************************************************* + ** + *******************************************************************************/ + BigDecimal getAverage(); +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/BigDecimalAggregates.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/BigDecimalAggregates.java new file mode 100644 index 00000000..ae66baf8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/BigDecimalAggregates.java @@ -0,0 +1,135 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.utils.aggregates; + + +import java.math.BigDecimal; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class BigDecimalAggregates implements AggregatesInterface +{ + private int count = 0; + // private Integer countDistinct; + private BigDecimal sum; + private BigDecimal min; + private BigDecimal max; + + + + /******************************************************************************* + ** Add a new value to this aggregate set + *******************************************************************************/ + public void add(BigDecimal input) + { + if(input == null) + { + return; + } + + count++; + + if(sum == null) + { + sum = input; + } + else + { + sum = sum.add(input); + } + + if(min == null || input.compareTo(min) < 0) + { + min = input; + } + + if(max == null || input.compareTo(max) > 0) + { + max = input; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int getCount() + { + return (count); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public BigDecimal getSum() + { + return (sum); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public BigDecimal getMin() + { + return (min); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public BigDecimal getMax() + { + return (max); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public BigDecimal getAverage() + { + if(this.count > 0) + { + return (BigDecimal.valueOf(this.sum.doubleValue() / (double) this.count)); + } + else + { + return (null); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/IntegerAggregates.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/IntegerAggregates.java new file mode 100644 index 00000000..c193f72a --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/IntegerAggregates.java @@ -0,0 +1,135 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.utils.aggregates; + + +import java.math.BigDecimal; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class IntegerAggregates implements AggregatesInterface +{ + private int count = 0; + // private Integer countDistinct; + private Integer sum; + private Integer min; + private Integer max; + + + + /******************************************************************************* + ** Add a new value to this aggregate set + *******************************************************************************/ + public void add(Integer input) + { + if(input == null) + { + return; + } + + count++; + + if(sum == null) + { + sum = input; + } + else + { + sum = sum + input; + } + + if(min == null || input < min) + { + min = input; + } + + if(max == null || input > max) + { + max = input; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int getCount() + { + return (count); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Integer getSum() + { + return (sum); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Integer getMin() + { + return (min); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Integer getMax() + { + return (max); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public BigDecimal getAverage() + { + if(this.count > 0) + { + return (BigDecimal.valueOf(this.sum.doubleValue() / (double) this.count)); + } + else + { + return (null); + } + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionTest.java similarity index 83% rename from qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportActionTest.java rename to qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionTest.java index aabef749..d1de143d 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionTest.java @@ -30,9 +30,9 @@ import java.util.List; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput; 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.fields.QFieldMetaData; @@ -50,7 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* ** Unit test for the ReportAction *******************************************************************************/ -class ReportActionTest +class ExportActionTest { /******************************************************************************* @@ -120,22 +120,22 @@ class ReportActionTest { try(FileOutputStream outputStream = new FileOutputStream(filename)) { - ReportInput reportInput = new ReportInput(TestUtils.defineInstance(), TestUtils.getMockSession()); - reportInput.setTableName("person"); - QTableMetaData table = reportInput.getTable(); + ExportInput exportInput = new ExportInput(TestUtils.defineInstance(), TestUtils.getMockSession()); + exportInput.setTableName("person"); + QTableMetaData table = exportInput.getTable(); - reportInput.setReportFormat(reportFormat); - reportInput.setReportOutputStream(outputStream); - reportInput.setQueryFilter(new QQueryFilter()); - reportInput.setLimit(recordCount); + exportInput.setReportFormat(reportFormat); + exportInput.setReportOutputStream(outputStream); + exportInput.setQueryFilter(new QQueryFilter()); + exportInput.setLimit(recordCount); if(specifyFields) { - reportInput.setFieldNames(table.getFields().values().stream().map(QFieldMetaData::getName).collect(Collectors.toList())); + exportInput.setFieldNames(table.getFields().values().stream().map(QFieldMetaData::getName).collect(Collectors.toList())); } - ReportOutput reportOutput = new ReportAction().execute(reportInput); - assertNotNull(reportOutput); - assertEquals(recordCount, reportOutput.getRecordCount()); + ExportOutput exportOutput = new ExportAction().execute(exportInput); + assertNotNull(exportOutput); + assertEquals(recordCount, exportOutput.getRecordCount()); } } @@ -147,12 +147,12 @@ class ReportActionTest @Test void testBadFieldNames() { - ReportInput reportInput = new ReportInput(TestUtils.defineInstance(), TestUtils.getMockSession()); - reportInput.setTableName("person"); - reportInput.setFieldNames(List.of("Foo", "Bar", "Baz")); + ExportInput exportInput = new ExportInput(TestUtils.defineInstance(), TestUtils.getMockSession()); + exportInput.setTableName("person"); + exportInput.setFieldNames(List.of("Foo", "Bar", "Baz")); assertThrows(QUserFacingException.class, () -> { - new ReportAction().execute(reportInput); + new ExportAction().execute(exportInput); }); } @@ -164,15 +164,15 @@ class ReportActionTest @Test void testPreExecuteCount() throws QException { - ReportInput reportInput = new ReportInput(TestUtils.defineInstance(), TestUtils.getMockSession()); - reportInput.setTableName("person"); + ExportInput exportInput = new ExportInput(TestUtils.defineInstance(), TestUtils.getMockSession()); + exportInput.setTableName("person"); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // use xlsx, which has a max-rows limit, to verify that code runs, but doesn't throw when there aren't too many rows // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - reportInput.setReportFormat(ReportFormat.XLSX); + exportInput.setReportFormat(ReportFormat.XLSX); - new ReportAction().preExecute(reportInput); + new ExportAction().preExecute(exportInput); //////////////////////////////////////////////////////////////////////////// // nothing to assert - but if preExecute throws, then the test will fail. // @@ -198,17 +198,17 @@ class ReportActionTest QInstance qInstance = TestUtils.defineInstance(); qInstance.addTable(wideTable); - ReportInput reportInput = new ReportInput(qInstance, TestUtils.getMockSession()); - reportInput.setTableName("wide"); + ExportInput exportInput = new ExportInput(qInstance, TestUtils.getMockSession()); + exportInput.setTableName("wide"); //////////////////////////////////////////////////////////////// // use xlsx, which has a max-cols limit, to verify that code. // //////////////////////////////////////////////////////////////// - reportInput.setReportFormat(ReportFormat.XLSX); + exportInput.setReportFormat(ReportFormat.XLSX); assertThrows(QUserFacingException.class, () -> { - new ReportAction().preExecute(reportInput); + new ExportAction().preExecute(exportInput); }); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java new file mode 100644 index 00000000..03bd10a7 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java @@ -0,0 +1,143 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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; + + +import java.math.BigDecimal; +import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QFormulaException; +import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.Test; +import static com.kingsrook.qqq.backend.core.actions.reporting.FormulaInterpreter.interpretFormula; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for FormulaInterpreter + *******************************************************************************/ +class FormulaInterpreterTest +{ + public static final Offset ZERO_OFFSET = Offset.offset(BigDecimal.ZERO); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInterpretFormulaSimpleSuccess() throws QFormulaException + { + QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter(); + + assertEquals(new BigDecimal("7"), interpretFormula(vi, "7")); + assertEquals(new BigDecimal("8"), interpretFormula(vi, "ADD(3,5)")); + assertEquals(new BigDecimal("9"), interpretFormula(vi, "ADD(2,ADD(3,4))")); + assertEquals(new BigDecimal("10"), interpretFormula(vi, "ADD(ADD(1,5),4)")); + assertEquals(new BigDecimal("11"), interpretFormula(vi, "ADD(ADD(1,5),ADD(2,3))")); + assertEquals(new BigDecimal("15"), interpretFormula(vi, "ADD(1,ADD(2,ADD(3,ADD(4,5))))")); + assertEquals(new BigDecimal("15"), interpretFormula(vi, "ADD(1,ADD(ADD(2,ADD(3,4)),5))")); + assertEquals(new BigDecimal("15"), interpretFormula(vi, "ADD(ADD(ADD(ADD(1,2),3),4),5)")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInterpretFormulaWithVariables() throws QFormulaException + { + QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter(); + vi.addValueMap("input", Map.of("i", 5, "j", 6, "f", new BigDecimal("0.1"))); + + assertEquals("5", interpretFormula(vi, "${input.i}")); + assertEquals(new BigDecimal("8"), interpretFormula(vi, "ADD(3,${input.i})")); + assertEquals(new BigDecimal("11"), interpretFormula(vi, "ADD(${input.i},${input.j})")); + assertEquals(new BigDecimal("11.1"), interpretFormula(vi, "ADD(${input.f},ADD(${input.i},${input.j}))")); + assertEquals(new BigDecimal("11.2"), interpretFormula(vi, "ADD(ADD(${input.f},ADD(${input.i},${input.j})),${input.f})")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInterpretFormulaRecursiveExceptions() + { + QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter(); + vi.addValueMap("input", Map.of("i", 5, "c", 'c')); + + assertThatThrownBy(() -> interpretFormula(vi, "")).hasMessageContaining("No results"); + assertThatThrownBy(() -> interpretFormula(vi, "NOT-A-FUN(1,2)")).hasMessageContaining("unrecognized expression"); + assertThatThrownBy(() -> interpretFormula(vi, "ADD(1)")).hasMessageContaining("Wrong number of arguments"); + assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,2,3)")).hasMessageContaining("Wrong number of arguments"); + assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,A)")).hasMessageContaining("[A] as a number"); + assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,${input.c})")).hasMessageContaining("[c] as a number"); + // todo - bad syntax (e.g., missing ')' + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFunctions() throws QFormulaException + { + QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter(); + + assertEquals(new BigDecimal("3"), interpretFormula(vi, "ADD(1,2)")); + assertEquals(new BigDecimal("2"), interpretFormula(vi, "MINUS(4,2)")); + assertEquals(new BigDecimal("34.500"), interpretFormula(vi, "MULTIPLY(100,0.345)")); + + assertThat((BigDecimal) interpretFormula(vi, "DIVIDE(1,2)")).isCloseTo(new BigDecimal("0.5"), ZERO_OFFSET); + assertNull(interpretFormula(vi, "DIVIDE(1,0)")); + + assertEquals(new BigDecimal("0.5"), interpretFormula(vi, "ROUND(0.510,1)")); + assertEquals(new BigDecimal("5.0"), interpretFormula(vi, "ROUND(5.010,2)")); + assertEquals(new BigDecimal("5"), interpretFormula(vi, "ROUND(5.010,1)")); + + assertEquals(new BigDecimal("0.5100"), interpretFormula(vi, "SCALE(0.510,4)")); + assertEquals(new BigDecimal("5.01"), interpretFormula(vi, "SCALE(5.010,2)")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QFormulaException + { + QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter(); + vi.addValueMap("pivot", Map.of("sum.noOfShoes", 5)); + vi.addValueMap("total", Map.of("sum.noOfShoes", 18)); + + assertEquals(new BigDecimal("27.78"), interpretFormula(vi, "SCALE(MULTIPLY(100,DIVIDE_SCALE(${pivot.sum.noOfShoes},${total.sum.noOfShoes},6)),2)")); + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..4dce7cf6 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java @@ -0,0 +1,442 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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; + + +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.Month; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; +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.fields.DisplayFormat; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +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.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; +import com.kingsrook.qqq.backend.core.testutils.PersonQRecord; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for GenerateReportAction + *******************************************************************************/ +class GenerateReportActionTest +{ + private static final String REPORT_NAME = "personReport1"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfterEach() + { + ListOfMapsExportStreamer.getList().clear(); + MemoryRecordStore.getInstance().reset(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPivot1() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + qInstance.addReport(defineReport(true)); + insertPersonRecords(qInstance); + runReport(qInstance, LocalDate.of(1980, Month.JANUARY, 1), LocalDate.of(1980, Month.DECEMBER, 31)); + + List> list = ListOfMapsExportStreamer.getList(); + Iterator> iterator = list.iterator(); + Map row = iterator.next(); + assertEquals(3, list.size()); + assertThat(list.get(0)).containsOnlyKeys("Last Name", "Report Start Date", "Report End Date", "Person Count", "Quantity", "Revenue", "Cost", "Profit", "Cost Per", "% Total", "Margins", "Revenue Per", "Margin Per"); + + assertThat(row.get("Last Name")).isEqualTo("Keller"); + assertThat(row.get("Person Count")).isEqualTo("1"); + assertThat(row.get("Quantity")).isEqualTo("5"); + assertThat(row.get("Report Start Date")).isEqualTo("1980-01-01"); + assertThat(row.get("Report End Date")).isEqualTo("1980-12-31"); + assertThat(row.get("Cost")).isEqualTo("3.50"); + assertThat(row.get("Revenue")).isEqualTo("2.40"); + assertThat(row.get("Cost Per")).isEqualTo("0.70"); + assertThat(row.get("Revenue Per")).isEqualTo("0.48"); + assertThat(row.get("Margin Per")).isEqualTo("-0.22"); + + row = iterator.next(); + assertThat(row.get("Last Name")).isEqualTo("Kelkhoff"); + assertThat(row.get("Person Count")).isEqualTo("2"); + assertThat(row.get("Quantity")).isEqualTo("13"); + assertThat(row.get("Cost")).isEqualTo("7.00"); // sum of the 2 Kelkhoff rows' costs + assertThat(row.get("Revenue")).isEqualTo("8.40"); // sum of the 2 Kelkhoff rows' price + assertThat(row.get("Cost Per")).isEqualTo("0.54"); // sum cost / quantity + assertThat(row.get("Revenue Per")).isEqualTo("0.65"); // sum price (Revenue) / quantity + assertThat(row.get("Margin Per")).isEqualTo("0.11"); // Revenue Per - Cost Per + + row = iterator.next(); + assertThat(row.get("Last Name")).isEqualTo("Totals"); + assertThat(row.get("Person Count")).isEqualTo("3"); + assertThat(row.get("Quantity")).isEqualTo("18"); + assertThat(row.get("Cost")).isEqualTo("10.50"); + assertThat(row.get("Cost Per")).startsWith("0.58"); + assertThat(row.get("Cost")).isEqualTo("10.50"); // sum of all 3 matching rows' costs + assertThat(row.get("Revenue")).isEqualTo("10.80"); // sum of all 3 matching rows' price + assertThat(row.get("Profit")).isEqualTo("0.30"); // Revenue - Cost + assertThat(row.get("Margins")).isEqualTo("0.03"); // 100*Profit / Revenue + assertThat(row.get("Cost Per")).isEqualTo("0.58"); // sum cost / quantity + assertThat(row.get("Revenue Per")).isEqualTo("0.60"); // sum price (Revenue) / quantity + assertThat(row.get("Margin Per")).isEqualTo("0.02"); // Revenue Per - Cost Per + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPivot2() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QReportMetaData report = defineReport(false); + + ////////////////////////////////////////////// + // change from the default to sort reversed // + ////////////////////////////////////////////// + report.getViews().get(0).getOrderByFields().get(0).setIsAscending(false); + qInstance.addReport(report); + insertPersonRecords(qInstance); + runReport(qInstance, LocalDate.of(1980, Month.JANUARY, 1), LocalDate.of(1980, Month.DECEMBER, 31)); + + List> list = ListOfMapsExportStreamer.getList(); + Iterator> iterator = list.iterator(); + Map row = iterator.next(); + assertEquals(2, list.size()); + + assertThat(row.get("Last Name")).isEqualTo("Kelkhoff"); + assertThat(row.get("Quantity")).isEqualTo("13"); + + row = iterator.next(); + assertThat(row.get("Last Name")).isEqualTo("Keller"); + assertThat(row.get("Quantity")).isEqualTo("5"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPivot3() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QReportMetaData report = defineReport(false); + + ////////////////////////////////////////////////////////////////////////////////////////////// + // remove the filters, change to sort by personCount (to get some ties), then sumPrice desc // + // this also shows the behavior of a null value in an order by // + ////////////////////////////////////////////////////////////////////////////////////////////// + report.setQueryFilter(null); + report.getViews().get(0).setOrderByFields(List.of(new QFilterOrderBy("personCount"), new QFilterOrderBy("sumPrice", false))); + qInstance.addReport(report); + insertPersonRecords(qInstance); + runReport(qInstance, LocalDate.now(), LocalDate.now()); + + List> list = ListOfMapsExportStreamer.getList(); + Iterator> iterator = list.iterator(); + Map row = iterator.next(); + + assertEquals(5, list.size()); + assertThat(row.get("Last Name")).isEqualTo("Keller"); + assertThat(row.get("Person Count")).isEqualTo("1"); + assertThat(row.get("Revenue")).isEqualTo("2.40"); + + row = iterator.next(); + assertThat(row.get("Last Name")).isEqualTo("Kelly"); + assertThat(row.get("Person Count")).isEqualTo("1"); + assertThat(row.get("Revenue")).isEqualTo("1.20"); + + row = iterator.next(); + assertThat(row.get("Last Name")).isEqualTo("Jones"); + assertThat(row.get("Person Count")).isEqualTo("1"); + assertThat(row.get("Revenue")).isEqualTo("1.00"); + + row = iterator.next(); + assertThat(row.get("Last Name")).isEqualTo("Jonson"); + assertThat(row.get("Person Count")).isEqualTo("1"); + assertThat(row.get("Revenue")).isNull(); + + row = iterator.next(); + assertThat(row.get("Last Name")).isEqualTo("Kelkhoff"); + assertThat(row.get("Person Count")).isEqualTo("2"); + assertThat(row.get("Revenue")).isEqualTo("8.40"); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPivot4() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QReportMetaData report = defineReport(false); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // remove the filter, change to have 2 pivot columns - homeStateId and lastName - we should get no roll-up like this. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + report.setQueryFilter(null); + report.getViews().get(0).setPivotFields(List.of( + "homeStateId", + "lastName" + )); + qInstance.addReport(report); + insertPersonRecords(qInstance); + runReport(qInstance, LocalDate.now(), LocalDate.now()); + + List> list = ListOfMapsExportStreamer.getList(); + Iterator> iterator = list.iterator(); + Map row = iterator.next(); + assertEquals(6, list.size()); + + assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Last Name")).isEqualTo("Jonson"); + assertThat(row.get("Quantity")).isNull(); + + row = iterator.next(); + assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Last Name")).isEqualTo("Jones"); + assertThat(row.get("Quantity")).isEqualTo("3"); + + row = iterator.next(); + assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Last Name")).isEqualTo("Kelly"); + assertThat(row.get("Quantity")).isEqualTo("4"); + + row = iterator.next(); + assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Last Name")).isEqualTo("Keller"); + assertThat(row.get("Quantity")).isEqualTo("5"); + + row = iterator.next(); + assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Last Name")).isEqualTo("Kelkhoff"); + assertThat(row.get("Quantity")).isEqualTo("6"); + + row = iterator.next(); + assertThat(row.get("Home State Id")).isEqualTo("2"); + assertThat(row.get("Last Name")).isEqualTo("Kelkhoff"); + assertThat(row.get("Quantity")).isEqualTo("7"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testPivot5() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QReportMetaData report = defineReport(false); + + ///////////////////////////////////////////////////////////////////////////////////// + // remove the filter, and just pivot on homeStateId - should aggregate differently // + ///////////////////////////////////////////////////////////////////////////////////// + report.setQueryFilter(null); + report.getViews().get(0).setPivotFields(List.of("homeStateId")); + qInstance.addReport(report); + insertPersonRecords(qInstance); + runReport(qInstance, LocalDate.now(), LocalDate.now()); + + List> list = ListOfMapsExportStreamer.getList(); + Iterator> iterator = list.iterator(); + Map row = iterator.next(); + assertEquals(2, list.size()); + assertThat(row.get("Home State Id")).isEqualTo("2"); + assertThat(row.get("Last Name")).isNull(); + assertThat(row.get("Quantity")).isEqualTo("7"); + + row = iterator.next(); + assertThat(row.get("Home State Id")).isEqualTo("1"); + assertThat(row.get("Last Name")).isNull(); + assertThat(row.get("Quantity")).isEqualTo("18"); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void runToCsv() throws Exception + { + String name = "/tmp/report.csv"; + try(FileOutputStream fileOutputStream = new FileOutputStream(name)) + { + QInstance qInstance = TestUtils.defineInstance(); + qInstance.addReport(defineReport(true)); + insertPersonRecords(qInstance); + + ReportInput reportInput = new ReportInput(qInstance); + reportInput.setSession(new QSession()); + reportInput.setReportName(REPORT_NAME); + reportInput.setReportFormat(ReportFormat.CSV); + reportInput.setReportOutputStream(fileOutputStream); + reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now())); + new GenerateReportAction().execute(reportInput); + System.out.println("Wrote File: " + name); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void runToXlsx() throws Exception + { + String name = "/tmp/report.xlsx"; + try(FileOutputStream fileOutputStream = new FileOutputStream(name)) + { + QInstance qInstance = TestUtils.defineInstance(); + qInstance.addReport(defineReport(true)); + insertPersonRecords(qInstance); + + ReportInput reportInput = new ReportInput(qInstance); + reportInput.setSession(new QSession()); + reportInput.setReportName(REPORT_NAME); + reportInput.setReportFormat(ReportFormat.XLSX); + reportInput.setReportOutputStream(fileOutputStream); + reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now())); + new GenerateReportAction().execute(reportInput); + System.out.println("Wrote File: " + name); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void runReport(QInstance qInstance, LocalDate startDate, LocalDate endDate) throws QException + { + ReportInput reportInput = new ReportInput(qInstance); + reportInput.setSession(new QSession()); + reportInput.setReportName(REPORT_NAME); + reportInput.setReportFormat(ReportFormat.LIST_OF_MAPS); + reportInput.setReportOutputStream(new ByteArrayOutputStream()); + reportInput.setInputValues(Map.of("startDate", startDate, "endDate", endDate)); + new GenerateReportAction().execute(reportInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void insertPersonRecords(QInstance qInstance) throws QException + { + TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of( + new PersonQRecord().withLastName("Jonson").withBirthDate(LocalDate.of(1980, Month.JANUARY, 31)).withNoOfShoes(null).withHomeStateId(1).withPrice(null).withCost(new BigDecimal("0.50")), // wrong last initial + new PersonQRecord().withLastName("Jones").withBirthDate(LocalDate.of(1980, Month.JANUARY, 31)).withNoOfShoes(3).withHomeStateId(1).withPrice(new BigDecimal("1.00")).withCost(new BigDecimal("0.50")), // wrong last initial + new PersonQRecord().withLastName("Kelly").withBirthDate(LocalDate.of(1979, Month.DECEMBER, 30)).withNoOfShoes(4).withHomeStateId(1).withPrice(new BigDecimal("1.20")).withCost(new BigDecimal("0.50")), // bad birthdate + new PersonQRecord().withLastName("Keller").withBirthDate(LocalDate.of(1980, Month.JANUARY, 7)).withNoOfShoes(5).withHomeStateId(1).withPrice(new BigDecimal("2.40")).withCost(new BigDecimal("3.50")), + new PersonQRecord().withLastName("Kelkhoff").withBirthDate(LocalDate.of(1980, Month.FEBRUARY, 15)).withNoOfShoes(6).withHomeStateId(1).withPrice(new BigDecimal("3.60")).withCost(new BigDecimal("3.50")), + new PersonQRecord().withLastName("Kelkhoff").withBirthDate(LocalDate.of(1980, Month.MARCH, 20)).withNoOfShoes(7).withHomeStateId(2).withPrice(new BigDecimal("4.80")).withCost(new BigDecimal("3.50")) + )); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QReportMetaData defineReport(boolean includeTotalRow) + { + return new QReportMetaData() + .withName(REPORT_NAME) + .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withInputFields(List.of( + new QFieldMetaData("startDate", QFieldType.DATE_TIME), + new QFieldMetaData("endDate", QFieldType.DATE_TIME) + )) + .withQueryFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.STARTS_WITH, List.of("K"))) + .withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.BETWEEN, List.of("${input.startDate}", "${input.endDate}"))) + ) + .withViews(List.of( + new QReportView() + .withName("pivot") + .withType(ReportType.PIVOT) + .withPivotFields(List.of("lastName")) + .withTotalRow(includeTotalRow) + .withTitleFormat("Number of shoes - people born between %s and %s - pivot on LastName, sort by Quantity, Revenue DESC") + .withTitleFields(List.of("${input.startDate}", "${input.endDate}")) + .withOrderByFields(List.of(new QFilterOrderBy("shoeCount"), new QFilterOrderBy("sumPrice", false))) + .withColumns(List.of( + new QReportField().withName("reportStartDate").withLabel("Report Start Date").withFormula("${input.startDate}"), + new QReportField().withName("reportEndDate").withLabel("Report End Date").withFormula("${input.endDate}"), + new QReportField().withName("personCount").withLabel("Person Count").withFormula("${pivot.count.id}").withDisplayFormat(DisplayFormat.COMMAS), + new QReportField().withName("shoeCount").withLabel("Quantity").withFormula("${pivot.sum.noOfShoes}").withDisplayFormat(DisplayFormat.COMMAS), + // new QReportField().withName("percentOfTotal").withLabel("% Total").withFormula("=MULTIPLY(100,DIVIDE(${pivot.sum.noOfShoes},${total.sum.noOfShoes}))").withDisplayFormat(DisplayFormat.PERCENT_POINT2), + new QReportField().withName("percentOfTotal").withLabel("% Total").withFormula("=DIVIDE(${pivot.sum.noOfShoes},${total.sum.noOfShoes})").withDisplayFormat(DisplayFormat.PERCENT_POINT2), + new QReportField().withName("sumCost").withLabel("Cost").withFormula("${pivot.sum.cost}").withDisplayFormat(DisplayFormat.CURRENCY), + new QReportField().withName("sumPrice").withLabel("Revenue").withFormula("${pivot.sum.price}").withDisplayFormat(DisplayFormat.CURRENCY), + new QReportField().withName("profit").withLabel("Profit").withFormula("=MINUS(${pivot.sum.price},${pivot.sum.cost})").withDisplayFormat(DisplayFormat.CURRENCY), + // new QReportField().withName("margin").withLabel("Margins").withFormula("=SCALE(MULTIPLY(100,DIVIDE(MINUS(${pivot.sum.price},${pivot.sum.cost}),${pivot.sum.price})),0)").withDisplayFormat(DisplayFormat.PERCENT), + new QReportField().withName("margin").withLabel("Margins").withFormula("=SCALE(DIVIDE(MINUS(${pivot.sum.price},${pivot.sum.cost}),${pivot.sum.price}),2)").withDisplayFormat(DisplayFormat.PERCENT), + new QReportField().withName("costPerShoe").withLabel("Cost Per").withFormula("=DIVIDE_SCALE(${pivot.sum.cost},${pivot.sum.noOfShoes},2)").withDisplayFormat(DisplayFormat.CURRENCY), + new QReportField().withName("revenuePerShoe").withLabel("Revenue Per").withFormula("=DIVIDE_SCALE(${pivot.sum.price},${pivot.sum.noOfShoes},2)").withDisplayFormat(DisplayFormat.CURRENCY), + new QReportField().withName("marginPer").withLabel("Margin Per").withFormula("=MINUS(DIVIDE_SCALE(${pivot.sum.price},${pivot.sum.noOfShoes},2),DIVIDE_SCALE(${pivot.sum.cost},${pivot.sum.noOfShoes},2))").withDisplayFormat(DisplayFormat.CURRENCY) + )) + )); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java index 27428209..341f58f3 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatterTest.java @@ -64,6 +64,15 @@ class QValueFormatterTest assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000"))); assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000)); + assertEquals("1%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT), 1)); + assertEquals("1%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT), new BigDecimal("1.0"))); + assertEquals("1.0%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT1), 1)); + assertEquals("1.1%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT1), new BigDecimal("1.1"))); + assertEquals("1.1%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT1), new BigDecimal("1.12"))); + assertEquals("1.00%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT2), 1)); + assertEquals("1.10%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT2), new BigDecimal("1.1"))); + assertEquals("1.12%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT2), new BigDecimal("1.12"))); + ////////////////////////////////////////////////// // this one flows through the exceptional cases // ////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java index 1e339199..8b61f22e 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.instances; +import java.math.BigDecimal; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; import org.junit.jupiter.api.AfterEach; @@ -162,6 +163,43 @@ class QMetaDataVariableInterpreterTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValueMaps() + { + QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter(); + variableInterpreter.addValueMap("input", Map.of("foo", "bar", "amount", new BigDecimal("3.50"))); + + assertEquals("bar", variableInterpreter.interpretForObject("${input.foo}")); + assertEquals(new BigDecimal("3.50"), variableInterpreter.interpretForObject("${input.amount}")); + assertEquals("${input.x}", variableInterpreter.interpretForObject("${input.x}")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMultipleValueMaps() + { + QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter(); + variableInterpreter.addValueMap("input", Map.of("amount", new BigDecimal("3.50"), "x", "y")); + variableInterpreter.addValueMap("others", Map.of("foo", "fu", "amount", new BigDecimal("1.75"))); + + assertEquals("${input.foo}", variableInterpreter.interpretForObject("${input.foo}")); + assertEquals("fu", variableInterpreter.interpretForObject("${others.foo}")); + assertEquals(new BigDecimal("3.50"), variableInterpreter.interpretForObject("${input.amount}")); + assertEquals(new BigDecimal("1.75"), variableInterpreter.interpretForObject("${others.amount}")); + assertEquals("y", variableInterpreter.interpretForObject("${input.x}")); + assertEquals("${others.x}", variableInterpreter.interpretForObject("${others.x}")); + assertEquals("${input.nil}", variableInterpreter.interpretForObject("${input.nil}")); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/testutils/PersonQRecord.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/testutils/PersonQRecord.java new file mode 100644 index 00000000..7f7bdf6f --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/testutils/PersonQRecord.java @@ -0,0 +1,81 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.testutils; + + +import java.math.BigDecimal; +import java.time.LocalDate; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class PersonQRecord extends QRecord +{ + public PersonQRecord withLastName(String lastName) + { + setValue("lastName", lastName); + return (this); + } + + + + public PersonQRecord withBirthDate(LocalDate birthDate) + { + setValue("birthDate", birthDate); + return (this); + } + + + + public PersonQRecord withNoOfShoes(Integer noOfShoes) + { + setValue("noOfShoes", noOfShoes); + return (this); + } + + + + public PersonQRecord withPrice(BigDecimal price) + { + setValue("price", price); + return (this); + } + + + + public PersonQRecord withCost(BigDecimal cost) + { + setValue("cost", cost); + return (this); + } + + + + public PersonQRecord withHomeStateId(int homeStateId) + { + setValue("homeStateId", homeStateId); + return (this); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index a29afce5..35039ce7 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -58,6 +58,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; +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; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; @@ -91,7 +92,7 @@ import org.apache.logging.log4j.Logger; /******************************************************************************* ** Utility class for backend-core test classes - ** + ** TODO - move to testutils package. *******************************************************************************/ public class TestUtils { @@ -406,6 +407,9 @@ public class TestUtils .withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STATE)) .withField(new QFieldMetaData("favoriteShapeId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_SHAPE)) .withField(new QFieldMetaData("customValue", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_CUSTOM)) + .withField(new QFieldMetaData("noOfShoes", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS)) + .withField(new QFieldMetaData("cost", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) + .withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)) ; } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesTest.java new file mode 100644 index 00000000..ff429dd7 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesTest.java @@ -0,0 +1,125 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.utils.aggregates; + + +import java.math.BigDecimal; +import org.assertj.core.data.Offset; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/******************************************************************************* + ** Unit test for Aggregates + *******************************************************************************/ +class AggregatesTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInteger() + { + IntegerAggregates aggregates = new IntegerAggregates(); + + assertEquals(0, aggregates.getCount()); + assertNull(aggregates.getMin()); + assertNull(aggregates.getMax()); + assertNull(aggregates.getSum()); + assertNull(aggregates.getAverage()); + + aggregates.add(5); + assertEquals(1, aggregates.getCount()); + assertEquals(5, aggregates.getMin()); + assertEquals(5, aggregates.getMax()); + assertEquals(5, aggregates.getSum()); + assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("5"), Offset.offset(BigDecimal.ZERO)); + + aggregates.add(10); + assertEquals(2, aggregates.getCount()); + assertEquals(5, aggregates.getMin()); + assertEquals(10, aggregates.getMax()); + assertEquals(15, aggregates.getSum()); + assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("7.5"), Offset.offset(BigDecimal.ZERO)); + + aggregates.add(15); + assertEquals(3, aggregates.getCount()); + assertEquals(5, aggregates.getMin()); + assertEquals(15, aggregates.getMax()); + assertEquals(30, aggregates.getSum()); + assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10"), Offset.offset(BigDecimal.ZERO)); + + aggregates.add(null); + assertEquals(3, aggregates.getCount()); + assertEquals(5, aggregates.getMin()); + assertEquals(15, aggregates.getMax()); + assertEquals(30, aggregates.getSum()); + assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10"), Offset.offset(BigDecimal.ZERO)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBigDecimal() + { + BigDecimalAggregates aggregates = new BigDecimalAggregates(); + + assertEquals(0, aggregates.getCount()); + assertNull(aggregates.getMin()); + assertNull(aggregates.getMax()); + assertNull(aggregates.getSum()); + assertNull(aggregates.getAverage()); + + BigDecimal bd51 = new BigDecimal("5.1"); + aggregates.add(bd51); + assertEquals(1, aggregates.getCount()); + assertEquals(bd51, aggregates.getMin()); + assertEquals(bd51, aggregates.getMax()); + assertEquals(bd51, aggregates.getSum()); + assertThat(aggregates.getAverage()).isCloseTo(bd51, Offset.offset(BigDecimal.ZERO)); + + BigDecimal bd101 = new BigDecimal("10.1"); + aggregates.add(new BigDecimal("10.1")); + assertEquals(2, aggregates.getCount()); + assertEquals(bd51, aggregates.getMin()); + assertEquals(bd101, aggregates.getMax()); + assertEquals(new BigDecimal("15.2"), aggregates.getSum()); + assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("7.6"), Offset.offset(BigDecimal.ZERO)); + + BigDecimal bd148 = new BigDecimal("14.8"); + aggregates.add(bd148); + + aggregates.add(null); + assertEquals(3, aggregates.getCount()); + assertEquals(bd51, aggregates.getMin()); + assertEquals(bd148, aggregates.getMax()); + assertEquals(new BigDecimal("30.0"), aggregates.getSum()); + assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10.0"), Offset.offset(BigDecimal.ZERO)); + } + +} \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index dad72655..8e01bb9b 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -40,7 +40,7 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.WidgetDataLoader; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; -import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction; +import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -61,8 +61,8 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInpu import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataOutput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput; @@ -671,7 +671,6 @@ public class QJavalinImplementation - /******************************************************************************* ** Load the data for a widget of a given name. *******************************************************************************/ @@ -692,6 +691,7 @@ public class QJavalinImplementation } + /******************************************************************************* ** *******************************************************************************/ @@ -756,22 +756,22 @@ public class QJavalinImplementation ///////////////////////////////////////////// // set up the report action's input object // ///////////////////////////////////////////// - ReportInput reportInput = new ReportInput(qInstance); - setupSession(context, reportInput); - reportInput.setTableName(tableName); - reportInput.setReportFormat(reportFormat); - reportInput.setFilename(filename); - reportInput.setLimit(limit); + ExportInput exportInput = new ExportInput(qInstance); + setupSession(context, exportInput); + exportInput.setTableName(tableName); + exportInput.setReportFormat(reportFormat); + exportInput.setFilename(filename); + exportInput.setLimit(limit); String fields = stringQueryParam(context, "fields"); if(StringUtils.hasContent(fields)) { - reportInput.setFieldNames(List.of(fields.split(","))); + exportInput.setFieldNames(List.of(fields.split(","))); } if(filter != null) { - reportInput.setQueryFilter(JsonUtils.toObject(filter, QQueryFilter.class)); + exportInput.setQueryFilter(JsonUtils.toObject(filter, QQueryFilter.class)); } /////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -782,10 +782,10 @@ public class QJavalinImplementation PipedOutputStream pipedOutputStream = new PipedOutputStream(); PipedInputStream pipedInputStream = new PipedInputStream(); pipedOutputStream.connect(pipedInputStream); - reportInput.setReportOutputStream(pipedOutputStream); + exportInput.setReportOutputStream(pipedOutputStream); - ReportAction reportAction = new ReportAction(); - reportAction.preExecute(reportInput); + ExportAction exportAction = new ExportAction(); + exportAction.preExecute(exportInput); ///////////////////////////////////////////////////////////////////////////////////////////////////// // start the async job. // @@ -795,7 +795,7 @@ public class QJavalinImplementation { try { - reportAction.execute(reportInput); + exportAction.execute(exportInput); return (true); } catch(Exception e) diff --git a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java index a59cada8..ae0eb6bd 100644 --- a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -38,7 +38,7 @@ import java.util.Optional; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; -import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction; +import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -58,9 +58,9 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; -import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput; import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping; import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QKeyBasedFieldMapping; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; @@ -633,25 +633,25 @@ public class QPicoCliImplementation ///////////////////////////////////////////// // set up the report action's input object // ///////////////////////////////////////////// - ReportInput reportInput = new ReportInput(qInstance); - reportInput.setSession(session); - reportInput.setTableName(tableName); - reportInput.setReportFormat(reportFormat); - reportInput.setFilename(filename); - reportInput.setReportOutputStream(outputStream); - reportInput.setLimit(subParseResult.matchedOptionValue("limit", null)); + ExportInput exportInput = new ExportInput(qInstance); + exportInput.setSession(session); + exportInput.setTableName(tableName); + exportInput.setReportFormat(reportFormat); + exportInput.setFilename(filename); + exportInput.setReportOutputStream(outputStream); + exportInput.setLimit(subParseResult.matchedOptionValue("limit", null)); - reportInput.setQueryFilter(generateQueryFilter(subParseResult)); + exportInput.setQueryFilter(generateQueryFilter(subParseResult)); String fieldNames = subParseResult.matchedOptionValue("--fieldNames", ""); if(StringUtils.hasContent(fieldNames)) { - reportInput.setFieldNames(Arrays.asList(fieldNames.split(","))); + exportInput.setFieldNames(Arrays.asList(fieldNames.split(","))); } - ReportOutput reportOutput = new ReportAction().execute(reportInput); + ExportOutput exportOutput = new ExportAction().execute(exportInput); - commandLine.getOut().println("Wrote " + reportOutput.getRecordCount() + " records to file " + filename); + commandLine.getOut().println("Wrote " + exportOutput.getRecordCount() + " records to file " + filename); return commandLine.getCommandSpec().exitCodeOnSuccess(); } finally