From 36307dba24642b2b19f208101e33b04501eb0da3 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 15 Jul 2024 17:02:26 -0500 Subject: [PATCH] CE-1405 Updates to qqq-reports: support for ReportCustomRecordSourceInterface --- .../reporting/GenerateReportAction.java | 65 ++++++++++---- .../ReportCustomRecordSourceInterface.java | 42 +++++++++ .../core/instances/QInstanceValidator.java | 19 +++- .../metadata/reporting/QReportDataSource.java | 39 ++++++++ .../reports/BasicRunReportProcess.java | 3 +- .../instances/QInstanceValidatorTest.java | 90 ++++++++++++++++--- 6 files changed, 221 insertions(+), 37 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportCustomRecordSourceInterface.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index a216e9e5..3d687344 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.reporting.customizers.DataSourceQueryInputCustomizer; +import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface; import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; @@ -302,10 +303,19 @@ public class GenerateReportAction extends AbstractQActionFunction reportFormat.getMaxRows()) + CountInput countInput = new CountInput(); + countInput.setTableName(dataSource.getSourceTable()); + countInput.setFilter(queryFilter); + countInput.setQueryJoins(cloneDataSourceQueryJoins(dataSource)); + CountOutput countOutput = new CountAction().execute(countInput); + + count = countOutput.getCount(); + } + + if(count != null) + { + countByDataSource.put(dataSource.getName(), count); + + if(reportFormat.getMaxRows() != null && count > reportFormat.getMaxRows()) { throw (new QUserFacingException("The requested report would include more rows (" - + String.format("%,d", countOutput.getCount()) + ") than the maximum allowed (" + + String.format("%,d", count) + ") than the maximum allowed (" + String.format("%,d", reportFormat.getMaxRows()) + ") for the selected file format (" + reportFormat + ").")); } } @@ -423,13 +444,19 @@ public class GenerateReportAction extends AbstractQActionFunction QContext.getQInstance().getTable(dataSource.getSourceTable()).getLabel(), Objects.requireNonNullElse(dataSource.getSourceTable(), "")); AtomicInteger consumedCount = new AtomicInteger(0); - ///////////////////////////////////////////////////////////////// - // run a record pipe loop, over the query for this data source // - ///////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////////// + // run a record pipe loop, over the query (or other data-supplier/source) for this data source // + ///////////////////////////////////////////////////////////////////////////////////////////////// RecordPipe recordPipe = new BufferedRecordPipe(1000); new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) -> { - if(dataSource.getSourceTable() != null) + if(dataSource.getCustomRecordSource() != null) + { + ReportCustomRecordSourceInterface recordSource = QCodeLoader.getAdHoc(ReportCustomRecordSourceInterface.class, dataSource.getCustomRecordSource()); + recordSource.execute(reportInput, recordPipe); + return (true); + } + else if(dataSource.getSourceTable() != null) { QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone(); setInputValuesInQueryFilter(reportInput, queryFilter); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportCustomRecordSourceInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportCustomRecordSourceInterface.java new file mode 100644 index 00000000..42b1749b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportCustomRecordSourceInterface.java @@ -0,0 +1,42 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.reporting.customizers; + + +import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; + + +/******************************************************************************* + ** Interface to be implemented to do a custom source of data for a report + ** (instead of just a query against a table). + *******************************************************************************/ +public interface ReportCustomRecordSourceInterface +{ + + /*************************************************************************** + ** Given the report input, put records into the pipe, for the report. + ***************************************************************************/ + void execute(ReportInput reportInput, RecordPipe recordPipe) throws QException; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index e8b4812a..19469abc 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface; import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; @@ -1659,9 +1660,12 @@ public class QInstanceValidator String dataSourceErrorPrefix = "Report " + reportName + " data source " + dataSource.getName() + " "; + boolean hasASource = false; + if(StringUtils.hasContent(dataSource.getSourceTable())) { - assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (exactly 1 is required)."); + hasASource = true; + assertCondition(dataSource.getStaticDataSupplier() == null, dataSourceErrorPrefix + "has both a sourceTable and a staticDataSupplier (not compatible together)."); if(assertCondition(qInstance.getTable(dataSource.getSourceTable()) != null, dataSourceErrorPrefix + "source table " + dataSource.getSourceTable() + " is not a table in this instance.")) { if(dataSource.getQueryFilter() != null) @@ -1670,14 +1674,21 @@ public class QInstanceValidator } } } - else if(dataSource.getStaticDataSupplier() != null) + + if(dataSource.getStaticDataSupplier() != null) { + assertCondition(dataSource.getCustomRecordSource() == null, dataSourceErrorPrefix + "has both a staticDataSupplier and a customRecordSource (not compatible together)."); + hasASource = true; validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getStaticDataSupplier(), Supplier.class); } - else + + if(dataSource.getCustomRecordSource() != null) { - errors.add(dataSourceErrorPrefix + "does not have a sourceTable or a staticDataSupplier (exactly 1 is required)."); + hasASource = true; + validateSimpleCodeReference(dataSourceErrorPrefix, dataSource.getCustomRecordSource(), ReportCustomRecordSourceInterface.class); } + + assertCondition(hasASource, dataSourceErrorPrefix + "does not have a sourceTable, customRecordSource, or a staticDataSupplier."); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java index 2473e921..843c5241 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java @@ -32,6 +32,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; /******************************************************************************* ** Meta-data definition of a source of data for a report (e.g., a table and query ** filter or custom-code reference). + ** + ** Runs in 3 modes: + ** + ** - If a customRecordSource is specified, then that code is executed to get the records. + ** - else, if a sourceTable is specified, then the corresponding queryFilter + ** (optionally along with queryJoins and queryInputCustomizer) is used. + ** - else a staticDataSupplier is used. *******************************************************************************/ public class QReportDataSource { @@ -44,6 +51,7 @@ public class QReportDataSource private QCodeReference queryInputCustomizer; private QCodeReference staticDataSupplier; + private QCodeReference customRecordSource; @@ -265,4 +273,35 @@ public class QReportDataSource return (this); } + + /******************************************************************************* + ** Getter for customRecordSource + *******************************************************************************/ + public QCodeReference getCustomRecordSource() + { + return (this.customRecordSource); + } + + + + /******************************************************************************* + ** Setter for customRecordSource + *******************************************************************************/ + public void setCustomRecordSource(QCodeReference customRecordSource) + { + this.customRecordSource = customRecordSource; + } + + + + /******************************************************************************* + ** Fluent setter for customRecordSource + *******************************************************************************/ + public QReportDataSource withCustomRecordSource(QCodeReference customRecordSource) + { + this.customRecordSource = customRecordSource; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcess.java index 3eb738db..aa2413df 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcess.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/BasicRunReportProcess.java @@ -46,7 +46,8 @@ public class BasicRunReportProcess public static final String STEP_NAME_EXECUTE = "execute"; public static final String STEP_NAME_ACCESS = "accessReport"; - public static final String FIELD_REPORT_NAME = "reportName"; + public static final String FIELD_REPORT_NAME = "reportName"; + public static final String FIELD_REPORT_FORMAT = "reportFormat"; diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 6f958c64..512cf7c5 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.instances; +import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -38,6 +39,8 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarCh import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ParentWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.processes.CancelProcessActionTest; +import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; +import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportCustomRecordSourceInterface; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; @@ -45,6 +48,7 @@ import com.kingsrook.qqq.backend.core.instances.validation.plugins.AlwaysFailsPr import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +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; @@ -1698,24 +1702,51 @@ public class QInstanceValidatorTest extends BaseTest @Test void testReportDataSourceStaticDataSupplier() { - assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).withStaticDataSupplier(new QCodeReference()), + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0).withStaticDataSupplier(new QCodeReference(TestReportStaticDataSupplier.class)), "has both a sourceTable and a staticDataSupplier"); assertValidationFailureReasons((qInstance) -> - { - QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); - dataSource.setSourceTable(null); - dataSource.setStaticDataSupplier(new QCodeReference(null, QCodeType.JAVA)); - }, - "missing a code reference name"); + { + QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); + dataSource.setSourceTable(null); + dataSource.setStaticDataSupplier(new QCodeReference(null, QCodeType.JAVA)); + }, "missing a code reference name"); assertValidationFailureReasons((qInstance) -> - { - QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); - dataSource.setSourceTable(null); - dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class)); - }, - "is not of the expected type"); + { + QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); + dataSource.setSourceTable(null); + dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class)); + }, "is not of the expected type"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportDataSourceCustomRecordSource() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0) + .withSourceTable(null) + .withStaticDataSupplier(new QCodeReference(TestReportStaticDataSupplier.class)) + .withCustomRecordSource(new QCodeReference(TestReportCustomRecordSource.class)), + "has both a staticDataSupplier and a customRecordSource"); + + assertValidationFailureReasons((qInstance) -> + { + QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); + dataSource.setSourceTable(null); + dataSource.setCustomRecordSource(new QCodeReference(null, QCodeType.JAVA)); + }, "missing a code reference name"); + + assertValidationFailureReasons((qInstance) -> + { + QReportDataSource dataSource = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getDataSources().get(0); + dataSource.setSourceTable(null); + dataSource.setCustomRecordSource(new QCodeReference(ArrayList.class)); + }, "is not of the expected type"); } @@ -2339,5 +2370,38 @@ public class QInstanceValidatorTest extends BaseTest *******************************************************************************/ public static class ValidAuthCustomizer implements QAuthenticationModuleCustomizerInterface {} + + /*************************************************************************** + ** + ***************************************************************************/ + public static class TestReportStaticDataSupplier implements Supplier>> + { + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public List> get() + { + return List.of(); + } + } + + + /*************************************************************************** + ** + ***************************************************************************/ + public static class TestReportCustomRecordSource implements ReportCustomRecordSourceInterface + { + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void execute(ReportInput reportInput, RecordPipe recordPipe) throws QException + { + + } + } }