diff --git a/.circleci/config.yml b/.circleci/config.yml
index fccbcbc3..39d8dc38 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -153,7 +153,7 @@ workflows:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
filters:
branches:
- ignore: /dev/
+ ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
@@ -163,12 +163,9 @@ workflows:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
filters:
branches:
- only: /dev/
+ only: /(dev|integration.*)/
tags:
only: /(version|snapshot)-.*/
-
- publish-docs:
- jobs:
- publish_asciidoc:
filters:
branches:
diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml
index 38bad7f5..26a4848f 100644
--- a/qqq-backend-core/pom.xml
+++ b/qqq-backend-core/pom.xml
@@ -102,6 +102,16 @@
fastexcel
0.12.15
+
+ org.apache.poi
+ poi
+ 5.2.5
+
+
+ org.apache.poi
+ poi-ooxml
+ 5.2.5
+
com.auth0
auth0
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java
index 6cc317d5..875097fa 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/async/AsyncJobManager.java
@@ -169,17 +169,24 @@ public class AsyncJobManager
LOG.debug("Completed job " + uuidAndTypeStateKey.getUuid());
return (result);
}
- catch(Exception e)
+ catch(Throwable t)
{
asyncJobStatus.setState(AsyncJobState.ERROR);
- asyncJobStatus.setCaughtException(e);
+ if(t instanceof Exception e)
+ {
+ asyncJobStatus.setCaughtException(e);
+ }
+ else
+ {
+ asyncJobStatus.setCaughtException(new QException("Caught throwable", t));
+ }
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
//////////////////////////////////////////////////////
// if user facing, just log an info, warn otherwise //
//////////////////////////////////////////////////////
- LOG.log((e instanceof QUserFacingException) ? Level.INFO : Level.WARN, "Job ended with an exception", e, logPair("jobId", uuidAndTypeStateKey.getUuid()));
- throw (new CompletionException(e));
+ LOG.log((t instanceof QUserFacingException) ? Level.INFO : Level.WARN, "Job ended with an exception", t, logPair("jobId", uuidAndTypeStateKey.getUuid()));
+ throw (new CompletionException(t));
}
finally
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java
index 3ab02ce4..044a24b7 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/DMLAuditAction.java
@@ -303,7 +303,7 @@ public class DMLAuditAction extends AbstractQActionFunction matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
- LOG.debug("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
+ LOG.debug("Of the [" + records.size() + "] records that were pending automations, [" + matchingQRecords.size() + "] of them match the filter on the action:" + action);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
@@ -601,7 +601,7 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** Finally, actually run action code against a list of known matching records.
- ** todo not commit - move to somewhere genericer
+ **
*******************************************************************************/
public static void applyActionToMatchingRecords(QTableMetaData table, List records, TableAutomationAction action) throws Exception
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java
index 14773be9..b4d46ca7 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java
@@ -96,7 +96,7 @@ public class QCodeLoader
}
catch(Exception e)
{
- LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
+ LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
@@ -135,7 +135,7 @@ public class QCodeLoader
}
catch(Exception e)
{
- LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
+ LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
@@ -187,7 +187,7 @@ public class QCodeLoader
}
catch(Exception e)
{
- LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
+ LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QStorageInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QStorageInterface.java
new file mode 100644
index 00000000..1181494b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QStorageInterface.java
@@ -0,0 +1,49 @@
+/*
+ * 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.interfaces;
+
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
+
+
+/*******************************************************************************
+ ** Interface for actions that a backend can perform, based on streaming data
+ ** into the backend's storage.
+ *******************************************************************************/
+public interface QStorageInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ OutputStream createOutputStream(StorageInput storageInput) throws QException;
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ InputStream getInputStream(StorageInput storageInput) throws QException;
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/QProcessCallbackFactory.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/QProcessCallbackFactory.java
index 977104c0..eb2f24d9 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/QProcessCallbackFactory.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/QProcessCallbackFactory.java
@@ -26,8 +26,15 @@ import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
+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.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
@@ -65,4 +72,46 @@ public class QProcessCallbackFactory
};
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QProcessCallback forRecordEntity(QRecordEntity entity)
+ {
+ return forRecord(entity.toQRecord());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QProcessCallback forRecord(QRecord record)
+ {
+ String primaryKeyField = "id";
+ if(StringUtils.hasContent(record.getTableName()))
+ {
+ primaryKeyField = QContext.getQInstance().getTable(record.getTableName()).getPrimaryKeyField();
+ }
+
+ Serializable primaryKeyValue = record.getValue(primaryKeyField);
+ if(primaryKeyValue == null)
+ {
+ throw (new QRuntimeException("Record did not have value in its primary key field [" + primaryKeyField + "]"));
+ }
+
+ return (forPrimaryKey(primaryKeyField, primaryKeyValue));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QProcessCallback forPrimaryKey(String fieldName, Serializable value)
+ {
+ return (forFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, value))));
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java
index 49ecd772..fac33e13 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java
@@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -65,12 +66,12 @@ public class CsvExportStreamer implements ExportStreamerInterface
**
*******************************************************************************/
@Override
- public void start(ExportInput exportInput, List fields, String label) throws QReportingException
+ public void start(ExportInput exportInput, List fields, String label, QReportView view) throws QReportingException
{
this.exportInput = exportInput;
this.fields = fields;
table = exportInput.getTable();
- outputStream = this.exportInput.getReportOutputStream();
+ outputStream = this.exportInput.getReportDestination().getReportOutputStream();
writeTitleAndHeader();
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java
index 5b3fd0d6..4d4d6bff 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java
@@ -52,6 +52,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
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.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
@@ -138,7 +139,7 @@ public class ExportAction
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// check if this report format has a max-rows limit -- if so, do a count to verify we're under the limit //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
- ReportFormat reportFormat = exportInput.getReportFormat();
+ ReportFormat reportFormat = exportInput.getReportDestination().getReportFormat();
verifyCountUnderMax(exportInput, backendModule, reportFormat);
preExecuteRan = true;
@@ -243,10 +244,19 @@ public class ExportAction
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// set up a report streamer, which will read rows from the pipe, and write formatted report rows to the output stream //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- ReportFormat reportFormat = exportInput.getReportFormat();
+ ReportFormat reportFormat = exportInput.getReportDestination().getReportFormat();
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
List fields = getFields(exportInput);
- reportStreamer.start(exportInput, fields, "Sheet 1");
+
+ //////////////////////////////////////////////////////////
+ // it seems we can pass a view with just a name in here //
+ //////////////////////////////////////////////////////////
+ List views = new ArrayList<>();
+ views.add(new QReportView()
+ .withName("export"));
+
+ reportStreamer.preRun(exportInput.getReportDestination(), views);
+ reportStreamer.start(exportInput, fields, "Sheet 1", views.get(0));
//////////////////////////////////////////
// run the query action as an async job //
@@ -335,7 +345,7 @@ public class ExportAction
try
{
- exportInput.getReportOutputStream().close();
+ exportInput.getReportDestination().getReportOutputStream().close();
}
catch(Exception e)
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java
index 473b3b34..4e2f7e02 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java
@@ -26,8 +26,10 @@ 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.actions.reporting.ReportDestination;
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.reporting.QReportView;
/*******************************************************************************
@@ -35,20 +37,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
*******************************************************************************/
public interface ExportStreamerInterface
{
- /*******************************************************************************
- ** Called once, before any rows are available. Meant to write a header, for example.
- *******************************************************************************/
- void start(ExportInput exportInput, List fields, String label) throws QReportingException;
/*******************************************************************************
- ** Called as records flow into the pipe.
- ******************************************************************************/
- void addRecords(List recordList) throws QReportingException;
-
- /*******************************************************************************
- ** Called once, after all rows are available. Meant to write a footer, or close resources, for example.
+ ** Called once, before any sheets are actually being produced.
*******************************************************************************/
- void finish() throws QReportingException;
+ default void preRun(ReportDestination reportDestination, List views) throws QReportingException
+ {
+ // noop in base class
+ }
/*******************************************************************************
**
@@ -58,6 +54,20 @@ public interface ExportStreamerInterface
// noop in base class
}
+ /*******************************************************************************
+ ** Called once per sheet, before any rows are available. Meant to write a
+ ** header, for example.
+ **
+ ** If multiple sheets are being created, there is no separate end-sheet call.
+ ** Rather, a new one will just get started...
+ *******************************************************************************/
+ void start(ExportInput exportInput, List fields, String label, QReportView view) throws QReportingException;
+
+ /*******************************************************************************
+ ** Called as records flow into the pipe.
+ ******************************************************************************/
+ void addRecords(List recordList) throws QReportingException;
+
/*******************************************************************************
**
*******************************************************************************/
@@ -65,4 +75,11 @@ public interface ExportStreamerInterface
{
addRecords(List.of(record));
}
+
+ /*******************************************************************************
+ ** Called after all sheets are complete. Meant to do a final write, or close
+ ** resources, for example.
+ *******************************************************************************/
+ void finish() throws QReportingException;
+
}
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 4f1fc32b..11a3c472 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
@@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.io.Serializable;
import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -32,18 +34,23 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
+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.ReportViewCustomizer;
+import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
+import com.kingsrook.qqq.backend.core.context.QContext;
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.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
@@ -51,6 +58,9 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp
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.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.JoinsContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@@ -68,12 +78,17 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.BackendStepPostRunInput;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.BackendStepPostRunOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
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.InstantAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
+import com.kingsrook.qqq.backend.core.utils.aggregates.LocalDateAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.LongAggregates;
+import com.kingsrook.qqq.backend.core.utils.aggregates.StringAggregates;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@@ -88,7 +103,7 @@ import com.kingsrook.qqq.backend.core.utils.aggregates.LongAggregates;
** - summaries (like pivot tables, but called summary to avoid confusion with "native" pivot tables),
** - native pivot tables (not initially supported, due to lack of support in fastexcel...).
*******************************************************************************/
-public class GenerateReportAction
+public class GenerateReportAction extends AbstractQActionFunction
{
private static final QLogger LOG = QLogger.getLogger(GenerateReportAction.class);
@@ -102,50 +117,67 @@ public class GenerateReportAction
// Aggregates: (count:47;sum:10,000;max:2,000;min:15) //
// salesSummaryReport > [(state:MO),(city:St.Louis)] > salePrice > (count:47;sum:10,000;max:2,000;min:15) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
- Map>>> summaryAggregates = new HashMap<>();
- Map>>> varianceAggregates = new HashMap<>();
+ Map>>> summaryAggregates = new HashMap<>();
+ Map>>> varianceAggregates = new HashMap<>();
- Map> totalAggregates = new HashMap<>();
- Map> varianceTotalAggregates = new HashMap<>();
+ Map> totalAggregates = new HashMap<>();
+ Map> varianceTotalAggregates = new HashMap<>();
- private QReportMetaData report;
- private ReportFormat reportFormat;
private ExportStreamerInterface reportStreamer;
+ private List dataSources;
+ private List views;
+
+ private Map countByDataSource = new HashMap<>();
/*******************************************************************************
**
*******************************************************************************/
- public void execute(ReportInput reportInput) throws QException
+ public ReportOutput execute(ReportInput reportInput) throws QException
{
- report = reportInput.getInstance().getReport(reportInput.getReportName());
- reportFormat = reportInput.getReportFormat();
+ ReportOutput reportOutput = new ReportOutput();
+ QReportMetaData report = getReportMetaData(reportInput);
+
+ this.views = report.getViews();
+ this.dataSources = report.getDataSources();
+
+ ReportFormat reportFormat = reportInput.getReportDestination().getReportFormat();
if(reportFormat == null)
{
throw new QException("Report format was not specified.");
}
- reportStreamer = reportFormat.newReportStreamer();
+
+ if(reportInput.getOverrideExportStreamerSupplier() != null)
+ {
+ reportStreamer = reportInput.getOverrideExportStreamerSupplier().get();
+ }
+ else
+ {
+ reportStreamer = reportFormat.newReportStreamer();
+ }
+
+ reportStreamer.preRun(reportInput.getReportDestination(), views);
////////////////////////////////////////////////////////////////////////////////////////////////
// foreach data source, do a query (possibly more than 1, if it goes to multiple table views) //
////////////////////////////////////////////////////////////////////////////////////////////////
- for(QReportDataSource dataSource : report.getDataSources())
+ for(QReportDataSource dataSource : dataSources)
{
//////////////////////////////////////////////////////////////////////////////
// make a list of the views that use this data source for various purposes. //
//////////////////////////////////////////////////////////////////////////////
- List dataSourceTableViews = report.getViews().stream()
+ List dataSourceTableViews = views.stream()
.filter(v -> v.getType().equals(ReportType.TABLE))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
- List dataSourceSummaryViews = report.getViews().stream()
+ List dataSourceSummaryViews = views.stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
- List dataSourceVariantViews = report.getViews().stream()
+ List dataSourceVariantViews = views.stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName()))
.toList();
@@ -184,24 +216,50 @@ public class GenerateReportAction
////////////////////////////////////////////////////////////////////////////////////
// start the table-view (e.g., open this tab in xlsx) and then run the query-loop //
////////////////////////////////////////////////////////////////////////////////////
- startTableView(reportInput, dataSource, dataSourceTableView);
+ startTableView(reportInput, dataSource, dataSourceTableView, reportFormat);
gatherData(reportInput, dataSource, dataSourceTableView, dataSourceSummaryViews, dataSourceVariantViews);
}
}
}
+ //////////////////////
+ // add pivot sheets //
+ //////////////////////
+ for(QReportView view : views)
+ {
+ if(view.getType().equals(ReportType.PIVOT))
+ {
+ if(reportFormat.getSupportsNativePivotTables())
+ {
+ startTableView(reportInput, null, view, reportFormat);
+ }
+ else
+ {
+ LOG.warn("Request to render a report with a PIVOT type view, for a format that does not support native pivot tables", logPair("reportFormat", reportFormat));
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // there's no data to add to a pivot table, so nothing else to do here. //
+ //////////////////////////////////////////////////////////////////////////
+ }
+ }
+
outputSummaries(reportInput);
+ reportOutput.setTotalRecordCount(countByDataSource.values().stream().mapToInt(Integer::intValue).sum());
+
reportStreamer.finish();
try
{
- reportInput.getReportOutputStream().close();
+ reportInput.getReportDestination().getReportOutputStream().close();
}
catch(Exception e)
{
throw (new QReportingException("Error completing report", e));
}
+
+ return (reportOutput);
}
@@ -209,28 +267,48 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
- private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QException
+ private QReportMetaData getReportMetaData(ReportInput reportInput) throws QException
{
- QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
+ if(reportInput.getReportMetaData() != null)
+ {
+ return reportInput.getReportMetaData();
+ }
+ if(StringUtils.hasContent(reportInput.getReportName()))
+ {
+ return QContext.getQInstance().getReport(reportInput.getReportName());
+ }
+
+ throw (new QReportingException("ReportInput did not contain required parameters to identify the report being generated"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView, ReportFormat reportFormat) throws QException
+ {
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
ExportInput exportInput = new ExportInput();
- exportInput.setReportFormat(reportFormat);
- exportInput.setFilename(reportInput.getFilename());
+ exportInput.setReportDestination(reportInput.getReportDestination());
exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow());
- exportInput.setReportOutputStream(reportInput.getReportOutputStream());
JoinsContext joinsContext = null;
- if(StringUtils.hasContent(dataSource.getSourceTable()))
+ if(dataSource != null)
{
- joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
+ if(StringUtils.hasContent(dataSource.getSourceTable()))
+ {
+ joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
+ countDataSourceRecords(reportInput, dataSource, reportFormat);
+ }
}
List fields = new ArrayList<>();
- for(QReportField column : reportView.getColumns())
+ for(QReportField column : CollectionUtils.nonNullList(reportView.getColumns()))
{
if(column.getIsVirtual())
{
@@ -242,7 +320,7 @@ public class GenerateReportAction
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext == null ? null : joinsContext.getFieldAndTableNameOrAlias(effectiveFieldName);
if(fieldAndTableNameOrAlias == null || fieldAndTableNameOrAlias.field() == null)
{
- throw new QReportingException("Could not find field named [" + effectiveFieldName + "] in dataSource [" + dataSource.getName() + "]");
+ throw new QReportingException("Could not find field named [" + effectiveFieldName + "] in dataSource [" + (dataSource == null ? null : dataSource.getName()) + "]");
}
QFieldMetaData field = fieldAndTableNameOrAlias.field().clone();
@@ -256,7 +334,7 @@ public class GenerateReportAction
}
reportStreamer.setDisplayFormats(getDisplayFormatMap(fields));
- reportStreamer.start(exportInput, fields, reportView.getLabel());
+ reportStreamer.start(exportInput, fields, reportView.getLabel(), reportView);
}
@@ -264,7 +342,36 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
- private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List summaryViews, List variantViews) throws QException
+ private void countDataSourceRecords(ReportInput reportInput, QReportDataSource dataSource, ReportFormat reportFormat) throws QException
+ {
+ QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
+ setInputValuesInQueryFilter(reportInput, queryFilter);
+
+ CountInput countInput = new CountInput();
+ countInput.setTableName(dataSource.getSourceTable());
+ countInput.setFilter(queryFilter);
+ countInput.setQueryJoins(dataSource.getQueryJoins());
+ CountOutput countOutput = new CountAction().execute(countInput);
+
+ if(countOutput.getCount() != null)
+ {
+ countByDataSource.put(dataSource.getName(), countOutput.getCount());
+
+ if(reportFormat.getMaxRows() != null && countOutput.getCount() > reportFormat.getMaxRows())
+ {
+ throw (new QUserFacingException("The requested report would include more rows ("
+ + String.format("%,d", countOutput.getCount()) + ") than the maximum allowed ("
+ + String.format("%,d", reportFormat.getMaxRows()) + ") for the selected file format (" + reportFormat + ")."));
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private Integer gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List summaryViews, List variantViews) throws QException
{
////////////////////////////////////////////////////////////////////////////////////////
// check if this view has a transform step - if so, set it up now and run its pre-run //
@@ -291,6 +398,9 @@ public class GenerateReportAction
RunBackendStepInput finalTransformStepInput = transformStepInput;
RunBackendStepOutput finalTransformStepOutput = transformStepOutput;
+ String tableLabel = ObjectUtils.tryElse(() -> 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 //
/////////////////////////////////////////////////////////////////
@@ -307,6 +417,7 @@ public class GenerateReportAction
queryInput.setTableName(dataSource.getSourceTable());
queryInput.setFilter(queryFilter);
queryInput.setQueryJoins(dataSource.getQueryJoins());
+ queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryInput.getFilter())));
@@ -350,6 +461,17 @@ public class GenerateReportAction
records = finalTransformStepOutput.getRecords();
}
+ Integer total = countByDataSource.get(dataSource.getName());
+ if(total != null)
+ {
+ reportInput.getAsyncJobCallback().updateStatus("Processing " + tableLabel + " records", consumedCount.get() + 1, total);
+ }
+ else
+ {
+ reportInput.getAsyncJobCallback().updateStatus("Processing " + tableLabel + " records (" + String.format("%,d", consumedCount.get() + 1) + ")");
+ }
+ consumedCount.getAndAdd(records.size());
+
return (consumeRecords(reportInput, dataSource, records, tableView, summaryViews, variantViews));
});
@@ -360,6 +482,8 @@ public class GenerateReportAction
{
transformStep.postRun(new BackendStepPostRunInput(transformStepInput), new BackendStepPostRunOutput(transformStepOutput));
}
+
+ return consumedCount.get();
}
@@ -367,11 +491,11 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
- private Set setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext)
+ private Set setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext) throws QException
{
Set fieldsToTranslatePossibleValues = new HashSet<>();
- for(QReportView view : report.getViews())
+ for(QReportView view : views)
{
for(QReportField column : CollectionUtils.nonNullList(view.getColumns()))
{
@@ -385,15 +509,16 @@ public class GenerateReportAction
}
}
- for(String summaryField : CollectionUtils.nonNullList(view.getPivotFields()))
+ for(String summaryFieldName : CollectionUtils.nonNullList(view.getSummaryFields()))
{
///////////////////////////////////////////////////////////////////////////////
// all pivotFields that are possible value sources are implicitly translated //
///////////////////////////////////////////////////////////////////////////////
- QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
- if(table.getField(summaryField).getPossibleValueSourceName() != null)
+ QTableMetaData mainTable = QContext.getQInstance().getTable(dataSource.getSourceTable());
+ FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(mainTable, summaryFieldName);
+ if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
{
- fieldsToTranslatePossibleValues.add(summaryField);
+ fieldsToTranslatePossibleValues.add(summaryFieldName);
}
}
}
@@ -403,6 +528,32 @@ public class GenerateReportAction
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
+ {
+ if(fieldName.indexOf('.') > -1)
+ {
+ String joinTableName = fieldName.replaceAll("\\..*", "");
+ String joinFieldName = fieldName.replaceAll(".*\\.", "");
+
+ QTableMetaData joinTable = QContext.getQInstance().getTable(joinTableName);
+ if(joinTable == null)
+ {
+ throw (new QException("Unrecognized join table name: " + joinTableName));
+ }
+
+ return new FieldAndJoinTable(joinTable.getField(joinFieldName), joinTable);
+ }
+ else
+ {
+ return new FieldAndJoinTable(mainTable.getField(fieldName), mainTable);
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -495,26 +646,27 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
- private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List records, Map>>> aggregatesMap)
+ private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List records, Map>>> aggregatesMap) throws QException
{
- Map>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
+ Map>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
for(QRecord record : records)
{
SummaryKey key = new SummaryKey();
- for(String summaryField : view.getPivotFields())
+ for(String summaryFieldName : view.getSummaryFields())
{
- Serializable summaryValue = record.getValue(summaryField);
- if(table.getField(summaryField).getPossibleValueSourceName() != null)
+ FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
+ Serializable summaryValue = record.getValue(summaryFieldName);
+ if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// so, this is kinda a thing - where we implicitly use possible-value labels (e.g., display values) for pivot fields... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- summaryValue = record.getDisplayValue(summaryField);
+ summaryValue = record.getDisplayValue(summaryFieldName);
}
- key.add(summaryField, summaryValue);
+ key.add(summaryFieldName, summaryValue);
- if(view.getIncludePivotSubTotals() && key.getKeys().size() < view.getPivotFields().size())
+ if(view.getIncludeSummarySubTotals() && key.getKeys().size() < view.getSummaryFields().size())
{
/////////////////////////////////////////////////////////////////////////////////////////
// be careful here, with these key objects, and their identity, being used as map keys //
@@ -533,9 +685,9 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
- private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map>> viewAggregates, SummaryKey key)
+ private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map>> viewAggregates, SummaryKey key) throws QException
{
- Map> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
+ Map> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
addRecordToAggregatesMap(table, record, keyAggregates);
}
@@ -544,29 +696,58 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
- private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map> aggregatesMap)
+ private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map> aggregatesMap) throws QException
{
- for(QFieldMetaData field : table.getFields().values())
+ //////////////////////////////////////////////////////////////////////////////////////
+ // todo - an optimization could be, to only compute aggregates that we'll need... //
+ // Only if we measure and see this to be slow - it may be, lots of BigDecimal math? //
+ //////////////////////////////////////////////////////////////////////////////////////
+ for(String fieldName : record.getValues().keySet())
{
- if(field.getType().equals(QFieldType.INTEGER))
+ FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
+ QFieldMetaData field = fieldAndJoinTable.field();
+ if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
@SuppressWarnings("unchecked")
- AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
- fieldAggregates.add(record.getValueInteger(field.getName()));
+ AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(fieldName, (name) -> new StringAggregates());
+ fieldAggregates.add(record.getDisplayValue(fieldName));
+ }
+ else if(field.getType().equals(QFieldType.INTEGER))
+ {
+ @SuppressWarnings("unchecked")
+ AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(fieldName, (name) -> new IntegerAggregates());
+ fieldAggregates.add(record.getValueInteger(fieldName));
}
else if(field.getType().equals(QFieldType.LONG))
{
@SuppressWarnings("unchecked")
- AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new LongAggregates());
- fieldAggregates.add(record.getValueLong(field.getName()));
+ AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(fieldName, (name) -> new LongAggregates());
+ fieldAggregates.add(record.getValueLong(fieldName));
}
else if(field.getType().equals(QFieldType.DECIMAL))
{
@SuppressWarnings("unchecked")
- AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
- fieldAggregates.add(record.getValueBigDecimal(field.getName()));
+ AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(fieldName, (name) -> new BigDecimalAggregates());
+ fieldAggregates.add(record.getValueBigDecimal(fieldName));
+ }
+ else if(field.getType().equals(QFieldType.DATE_TIME))
+ {
+ @SuppressWarnings("unchecked")
+ AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(fieldName, (name) -> new InstantAggregates());
+ fieldAggregates.add(record.getValueInstant(fieldName));
+ }
+ else if(field.getType().equals(QFieldType.DATE))
+ {
+ @SuppressWarnings("unchecked")
+ AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(fieldName, (name) -> new LocalDateAggregates());
+ fieldAggregates.add(record.getValueLocalDate(fieldName));
+ }
+ if(field.getType().isStringLike())
+ {
+ @SuppressWarnings("unchecked")
+ AggregatesInterface fieldAggregates = (AggregatesInterface) aggregatesMap.computeIfAbsent(fieldName, (name) -> new StringAggregates());
+ fieldAggregates.add(record.getValueString(fieldName));
}
- // todo - more types (dates, at least?)
}
}
@@ -575,24 +756,22 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
- private void outputSummaries(ReportInput reportInput) throws QReportingException, QFormulaException
+ private void outputSummaries(ReportInput reportInput) throws QException
{
- List reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
+ List reportViews = views.stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
for(QReportView view : reportViews)
{
- QReportDataSource dataSource = report.getDataSource(view.getDataSourceName());
+ QReportDataSource dataSource = getDataSource(view.getDataSourceName());
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
ExportInput exportInput = new ExportInput();
- exportInput.setReportFormat(reportFormat);
- exportInput.setFilename(reportInput.getFilename());
+ exportInput.setReportDestination(reportInput.getReportDestination());
exportInput.setTitleRow(summaryOutput.titleRow);
exportInput.setIncludeHeaderRow(view.getIncludeHeaderRow());
- exportInput.setReportOutputStream(reportInput.getReportOutputStream());
reportStreamer.setDisplayFormats(getDisplayFormatMap(view));
- reportStreamer.start(exportInput, getFields(table, view), view.getLabel());
+ reportStreamer.start(exportInput, getFields(table, view), view.getLabel(), view);
reportStreamer.addRecords(summaryOutput.summaryRows); // todo - what if this set is huge?
@@ -605,6 +784,24 @@ public class GenerateReportAction
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QReportDataSource getDataSource(String dataSourceName)
+ {
+ for(QReportDataSource dataSource : CollectionUtils.nonNullList(dataSources))
+ {
+ if(dataSource.getName().equals(dataSourceName))
+ {
+ return (dataSource);
+ }
+ }
+
+ return (null);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -632,13 +829,13 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
- private List getFields(QTableMetaData table, QReportView view)
+ private List getFields(QTableMetaData table, QReportView view) throws QException
{
List fields = new ArrayList<>();
- for(String pivotField : view.getPivotFields())
+ for(String summaryFieldName : view.getSummaryFields())
{
- 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
+ FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
+ fields.add(new QFieldMetaData(summaryFieldName, fieldAndJoinTable.field().getType()).withLabel(fieldAndJoinTable.field().getLabel())); // todo do we need the type? if so need table as input here
}
for(QReportField column : view.getColumns())
{
@@ -668,11 +865,11 @@ public class GenerateReportAction
// create summary rows //
/////////////////////////
List summaryRows = new ArrayList<>();
- for(Map.Entry>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
+ for(Map.Entry>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
{
- SummaryKey summaryKey = entry.getKey();
- Map> fieldAggregates = entry.getValue();
- Map summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
+ SummaryKey summaryKey = entry.getKey();
+ Map> fieldAggregates = entry.getValue();
+ Map summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
variableInterpreter.addValueMap("pivot", summaryValues);
variableInterpreter.addValueMap("summary", summaryValues);
@@ -681,9 +878,9 @@ public class GenerateReportAction
if(!varianceAggregates.isEmpty())
{
- Map>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
- Map> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
- Map varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
+ Map>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
+ Map> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
+ Map varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
variableInterpreter.addValueMap("variancePivot", varianceValues);
variableInterpreter.addValueMap("variance", varianceValues);
}
@@ -702,7 +899,7 @@ public class GenerateReportAction
///////////////////////////////////////////////////////////////////////////////
// for summary subtotals, add the text "Total" to the last field in this key //
///////////////////////////////////////////////////////////////////////////////
- if(summaryKey.getKeys().size() < view.getPivotFields().size())
+ if(summaryKey.getKeys().size() < view.getSummaryFields().size())
{
String fieldName = summaryKey.getKeys().get(summaryKey.getKeys().size() - 1).getA();
summaryRow.setValue(fieldName, summaryRow.getValueString(fieldName) + " Total");
@@ -740,11 +937,11 @@ public class GenerateReportAction
{
totalRow = new QRecord();
- for(String pivotField : view.getPivotFields())
+ for(String summaryField : view.getSummaryFields())
{
if(totalRow.getValues().isEmpty())
{
- totalRow.setValue(pivotField, "Totals");
+ totalRow.setValue(summaryField, "Totals");
}
}
@@ -864,18 +1061,24 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
- private Map getSummaryValuesForInterpreter(Map> fieldAggregates)
+ private Map getSummaryValuesForInterpreter(Map> fieldAggregates)
{
Map summaryValuesForInterpreter = new HashMap<>();
- for(Map.Entry> subEntry : fieldAggregates.entrySet())
+ for(Map.Entry> subEntry : fieldAggregates.entrySet())
{
- String fieldName = subEntry.getKey();
- AggregatesInterface> aggregates = subEntry.getValue();
+ String fieldName = subEntry.getKey();
+ AggregatesInterface, ?> aggregates = subEntry.getValue();
summaryValuesForInterpreter.put("sum." + fieldName, aggregates.getSum());
summaryValuesForInterpreter.put("count." + fieldName, aggregates.getCount());
+ summaryValuesForInterpreter.put("count_nums." + fieldName, aggregates.getCount());
summaryValuesForInterpreter.put("min." + fieldName, aggregates.getMin());
summaryValuesForInterpreter.put("max." + fieldName, aggregates.getMax());
summaryValuesForInterpreter.put("average." + fieldName, aggregates.getAverage());
+ summaryValuesForInterpreter.put("product." + fieldName, aggregates.getProduct());
+ summaryValuesForInterpreter.put("var." + fieldName, aggregates.getVariance());
+ summaryValuesForInterpreter.put("varp." + fieldName, aggregates.getVarP());
+ summaryValuesForInterpreter.put("std_dev." + fieldName, aggregates.getStandardDeviation());
+ summaryValuesForInterpreter.put("std_devp." + fieldName, aggregates.getStdDevP());
}
return summaryValuesForInterpreter;
}
@@ -889,4 +1092,27 @@ public class GenerateReportAction
{
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
+ {
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public String getLabel(QTableMetaData mainTable)
+ {
+ if(mainTable.getName().equals(joinTable.getName()))
+ {
+ return (field.getLabel());
+ }
+ else
+ {
+ return (joinTable.getLabel() + ": " + field.getLabel());
+ }
+ }
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/JsonExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/JsonExportStreamer.java
index b90cde16..0b798fe8 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/JsonExportStreamer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/JsonExportStreamer.java
@@ -26,17 +26,23 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
-import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
/*******************************************************************************
@@ -46,13 +52,23 @@ public class JsonExportStreamer implements ExportStreamerInterface
{
private static final QLogger LOG = QLogger.getLogger(JsonExportStreamer.class);
+ private boolean prettyPrint = true;
+
private ExportInput exportInput;
private QTableMetaData table;
private List fields;
private OutputStream outputStream;
- private boolean needComma = false;
- private boolean prettyPrint = true;
+ private boolean multipleViews = false;
+ private boolean haveStartedAnyViews = false;
+
+ private boolean needCommaBeforeRecord = false;
+
+ private byte[] indent = new byte[0];
+ private String indentString = "";
+
+ private Pattern colonLetterPattern = Pattern.compile(":([A-Z]+)($|[A-Z][a-z])");
+ private Memoization fieldLabelMemoization = new Memoization<>();
@@ -69,21 +85,124 @@ public class JsonExportStreamer implements ExportStreamerInterface
**
*******************************************************************************/
@Override
- public void start(ExportInput exportInput, List fields, String label) throws QReportingException
+ public void preRun(ReportDestination reportDestination, List views) throws QReportingException
+ {
+ outputStream = reportDestination.getReportOutputStream();
+
+ if(views.size() > 1)
+ {
+ multipleViews = true;
+ }
+
+ if(multipleViews)
+ {
+ try
+ {
+ indentIfPretty(outputStream);
+ outputStream.write('[');
+ newlineIfPretty(outputStream);
+ increaseIndent();
+ }
+ catch(IOException e)
+ {
+ throw (new QReportingException("Error starting report output", e));
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void start(ExportInput exportInput, List fields, String label, QReportView view) throws QReportingException
{
this.exportInput = exportInput;
this.fields = fields;
table = exportInput.getTable();
- outputStream = this.exportInput.getReportOutputStream();
+
+ needCommaBeforeRecord = false;
try
{
+ if(multipleViews)
+ {
+ if(haveStartedAnyViews)
+ {
+ /////////////////////////
+ // close the last view //
+ /////////////////////////
+ newlineIfPretty(outputStream);
+
+ decreaseIndent();
+ indentIfPretty(outputStream);
+ outputStream.write(']');
+ newlineIfPretty(outputStream);
+
+ decreaseIndent();
+ indentIfPretty(outputStream);
+ outputStream.write('}');
+ outputStream.write(',');
+ newlineIfPretty(outputStream);
+ }
+
+ /////////////////////////////////////////////////////////////
+ // open a new view, as an object, with a name & data entry //
+ /////////////////////////////////////////////////////////////
+ indentIfPretty(outputStream);
+ outputStream.write('{');
+ newlineIfPretty(outputStream);
+ increaseIndent();
+
+ indentIfPretty(outputStream);
+ outputStream.write(String.format("""
+ "name":"%s",""", label).getBytes(StandardCharsets.UTF_8));
+ newlineIfPretty(outputStream);
+
+ indentIfPretty(outputStream);
+ outputStream.write("""
+ "data":""".getBytes(StandardCharsets.UTF_8));
+ newlineIfPretty(outputStream);
+ }
+
+ //////////////////////////////////////////////
+ // start the array of entries for this view //
+ //////////////////////////////////////////////
+ indentIfPretty(outputStream);
outputStream.write('[');
+ increaseIndent();
}
catch(IOException e)
{
throw (new QReportingException("Error starting report output", e));
}
+
+ haveStartedAnyViews = true;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void increaseIndent()
+ {
+ indent = new byte[indent.length + 3];
+ Arrays.fill(indent, (byte) ' ');
+ indentString = new String(indent);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void decreaseIndent()
+ {
+ indent = new byte[Math.max(0, indent.length - 3)];
+ Arrays.fill(indent, (byte) ' ');
+ indentString = new String(indent);
}
@@ -111,7 +230,7 @@ public class JsonExportStreamer implements ExportStreamerInterface
{
try
{
- if(needComma)
+ if(needCommaBeforeRecord)
{
outputStream.write(',');
}
@@ -119,21 +238,25 @@ public class JsonExportStreamer implements ExportStreamerInterface
Map mapForJson = new LinkedHashMap<>();
for(QFieldMetaData field : fields)
{
- String labelForJson = StringUtils.lcFirst(field.getLabel().replace(" ", ""));
- mapForJson.put(labelForJson, qRecord.getValue(field.getName()));
+ mapForJson.put(getLabelForJson(field), qRecord.getValue(field.getName()));
}
String json = prettyPrint ? JsonUtils.toPrettyJson(mapForJson) : JsonUtils.toJson(mapForJson);
+ if(prettyPrint)
+ {
+ json = json.replaceAll("(?s)\n", "\n" + indentString);
+ }
if(prettyPrint)
{
outputStream.write('\n');
}
+ indentIfPretty(outputStream);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush(); // todo - less often?
- needComma = true;
+ needCommaBeforeRecord = true;
}
catch(Exception e)
{
@@ -143,6 +266,73 @@ public class JsonExportStreamer implements ExportStreamerInterface
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ String getLabelForJson(QFieldMetaData field)
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // memoize, to avoid running these regex/replacements millions of times //
+ //////////////////////////////////////////////////////////////////////////
+ Optional result = fieldLabelMemoization.getResult(field.getName(), fieldName ->
+ {
+ String labelForJson = field.getLabel().replace(" ", "");
+
+ /////////////////////////////////////////////////////////////////////////////
+ // now fix up any field-name-parts after the table: portion of a join name //
+ // lineItem:SKU to become lineItem:sku //
+ // parcel:SLAStatus to become parcel:slaStatus //
+ // order:Client to become order:client //
+ /////////////////////////////////////////////////////////////////////////////
+ Matcher allCaps = Pattern.compile("^[A-Z]+$").matcher(labelForJson);
+ Matcher startsAllCapsThenNextWordMatcher = Pattern.compile("([A-Z]+)([A-Z][a-z].*)").matcher(labelForJson);
+ Matcher startsOneCapMatcher = Pattern.compile("([A-Z])(.*)").matcher(labelForJson);
+
+ if(allCaps.matches())
+ {
+ labelForJson = allCaps.replaceAll(m -> m.group().toLowerCase());
+ }
+ else if(startsAllCapsThenNextWordMatcher.matches())
+ {
+ labelForJson = startsAllCapsThenNextWordMatcher.replaceAll(m -> m.group(1).toLowerCase() + m.group(2));
+ }
+ else if(startsOneCapMatcher.matches())
+ {
+ labelForJson = startsOneCapMatcher.replaceAll(m -> m.group(1).toLowerCase() + m.group(2));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////
+ // now fix up any field-name-parts after the table: portion of a join name //
+ // lineItem:SKU to become lineItem:sku //
+ // parcel:SLAStatus to become parcel:slaStatus //
+ // order:Client to become order:client //
+ /////////////////////////////////////////////////////////////////////////////
+ Matcher colonThenAllCapsThenEndMatcher = Pattern.compile("(.*:)([A-Z]+)$").matcher(labelForJson);
+ Matcher colonThenAllCapsThenNextWordMatcher = Pattern.compile("(.*:)([A-Z]+)([A-Z][a-z].*)").matcher(labelForJson);
+ Matcher colonThenOneCapMatcher = Pattern.compile("(.*:)([A-Z])(.*)").matcher(labelForJson);
+
+ if(colonThenAllCapsThenEndMatcher.matches())
+ {
+ labelForJson = colonThenAllCapsThenEndMatcher.replaceAll(m -> m.group(1) + m.group(2).toLowerCase());
+ }
+ else if(colonThenAllCapsThenNextWordMatcher.matches())
+ {
+ labelForJson = colonThenAllCapsThenNextWordMatcher.replaceAll(m -> m.group(1) + m.group(2).toLowerCase() + m.group(3));
+ }
+ else if(colonThenOneCapMatcher.matches())
+ {
+ labelForJson = colonThenOneCapMatcher.replaceAll(m -> m.group(1) + m.group(2).toLowerCase() + m.group(3));
+ }
+
+ System.out.println("Label: " + labelForJson);
+ return (labelForJson);
+ });
+
+ return result.orElse(field.getName());
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -162,11 +352,34 @@ public class JsonExportStreamer implements ExportStreamerInterface
{
try
{
- if(prettyPrint)
- {
- outputStream.write('\n');
- }
+ //////////////////////////////////////////////
+ // close the array of entries for this view //
+ //////////////////////////////////////////////
+ newlineIfPretty(outputStream);
+
+ decreaseIndent();
+ indentIfPretty(outputStream);
outputStream.write(']');
+ newlineIfPretty(outputStream);
+
+ if(multipleViews)
+ {
+ ////////////////////////////////////////////
+ // close this view, if there are multiple //
+ ////////////////////////////////////////////
+ decreaseIndent();
+ indentIfPretty(outputStream);
+ outputStream.write('}');
+ newlineIfPretty(outputStream);
+
+ /////////////////////////////
+ // close the list of views //
+ /////////////////////////////
+ decreaseIndent();
+ indentIfPretty(outputStream);
+ outputStream.write(']');
+ newlineIfPretty(outputStream);
+ }
}
catch(IOException e)
{
@@ -174,4 +387,30 @@ public class JsonExportStreamer implements ExportStreamerInterface
}
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void newlineIfPretty(OutputStream outputStream) throws IOException
+ {
+ if(prettyPrint)
+ {
+ outputStream.write('\n');
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void indentIfPretty(OutputStream outputStream) throws IOException
+ {
+ if(prettyPrint)
+ {
+ outputStream.write(indent);
+ }
+ }
+
}
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
index 365c3a40..7e422a21 100644
--- 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
@@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
/*******************************************************************************
@@ -87,7 +88,7 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
**
*******************************************************************************/
@Override
- public void start(ExportInput exportInput, List fields, String label) throws QReportingException
+ public void start(ExportInput exportInput, List fields, String label, QReportView view) throws QReportingException
{
this.exportInput = exportInput;
this.fields = fields;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java
index 2583855f..b3cb59a1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java
@@ -138,7 +138,7 @@ public class RecordPipe
{
if(now - sleepLoopStartTime > MAX_SLEEP_LOOP_MILLIS)
{
- LOG.warn("Giving up adding record to pipe, due to pipe being full for more than {} millis", MAX_SLEEP_LOOP_MILLIS);
+ LOG.warn("Giving up adding record to pipe, due to pipe being full for more than " + MAX_SLEEP_LOOP_MILLIS + " millis");
throw (new IllegalStateException("Giving up adding record to pipe, due to pipe staying full too long."));
}
LOG.trace("Record pipe.add failed (due to full pipe). Blocking.");
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportUtils.java
new file mode 100644
index 00000000..9c682e22
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportUtils.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+
+import java.util.List;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ReportUtils
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QReportView getSourceViewForPivotTableView(List views, QReportView pivotTableView) throws QReportingException
+ {
+ Optional sourceView = views.stream().filter(v -> v.getName().equals(pivotTableView.getPivotTableSourceViewName())).findFirst();
+ if(sourceView.isEmpty())
+ {
+ throw (new QReportingException("Could not find data view [" + pivotTableView.getPivotTableSourceViewName() + "] for pivot table view [" + pivotTableView.getName() + "]"));
+ }
+
+ return sourceView.get();
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStyler.java
similarity index 85%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStyler.java
index 12dc6685..45d37640 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/BoldHeaderAndFooterFastExcelStyler.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2022. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
import org.dhatim.fastexcel.BorderSide;
@@ -30,7 +30,7 @@ import org.dhatim.fastexcel.StyleSetter;
/*******************************************************************************
** Version of excel styler that does bold headers and footers, with basic borders.
*******************************************************************************/
-public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
+public class BoldHeaderAndFooterFastExcelStyler implements FastExcelStylerInterface
{
/*******************************************************************************
@@ -60,6 +60,9 @@ public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
+ /*******************************************************************************
+ **
+ *******************************************************************************/
@Override
public void styleTotalsRow(StyleSetter totalsRowStyle)
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/ExcelFastexcelExportStreamer.java
similarity index 89%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/ExcelFastexcelExportStreamer.java
index 61f14588..fd1dc915 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/ExcelFastexcelExportStreamer.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2022. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.actions.reporting;
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
import java.io.OutputStream;
@@ -34,8 +34,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.ExcelStylerInterface;
-import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler;
+import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
+import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
@@ -43,7 +43,9 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.dhatim.fastexcel.StyleSetter;
@@ -52,19 +54,19 @@ import org.dhatim.fastexcel.Worksheet;
/*******************************************************************************
- ** Excel export format implementation
+ ** Excel export format implementation - built using fastexcel library
*******************************************************************************/
-public class ExcelExportStreamer implements ExportStreamerInterface
+public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
{
- private static final QLogger LOG = QLogger.getLogger(ExcelExportStreamer.class);
+ private static final QLogger LOG = QLogger.getLogger(ExcelFastexcelExportStreamer.class);
private ExportInput exportInput;
private QTableMetaData table;
private List fields;
private OutputStream outputStream;
- private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
- private Map excelCellFormats;
+ private FastExcelStylerInterface fastExcelStylerInterface = new PlainFastExcelStyler();
+ private Map excelCellFormats;
private Workbook workbook;
private Worksheet worksheet;
@@ -76,7 +78,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
/*******************************************************************************
**
*******************************************************************************/
- public ExcelExportStreamer()
+ public ExcelFastexcelExportStreamer()
{
}
@@ -105,14 +107,14 @@ public class ExcelExportStreamer implements ExportStreamerInterface
** Starts a new worksheet in the current workbook. Can be called multiple times.
*******************************************************************************/
@Override
- public void start(ExportInput exportInput, List fields, String label) throws QReportingException
+ public void start(ExportInput exportInput, List fields, String label, QReportView view) throws QReportingException
{
try
{
this.exportInput = exportInput;
this.fields = fields;
table = exportInput.getTable();
- outputStream = this.exportInput.getReportOutputStream();
+ outputStream = this.exportInput.getReportDestination().getReportOutputStream();
this.row = 0;
this.sheetCount++;
@@ -121,7 +123,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
/////////////////////////////////////////////////////////////////////////////////////////////////////
if(workbook == null)
{
- String appName = "QQQ";
+ String appName = ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getBranding().getAppName(), "QQQ");
QInstance instance = exportInput.getInstance();
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
{
@@ -167,7 +169,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
worksheet.range(row, 0, row, fields.size() - 1).merge();
StyleSetter titleStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
- excelStylerInterface.styleTitleRow(titleStyle);
+ fastExcelStylerInterface.styleTitleRow(titleStyle);
titleStyle.set();
row++;
@@ -187,7 +189,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
}
StyleSetter headerStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
- excelStylerInterface.styleHeaderRow(headerStyle);
+ fastExcelStylerInterface.styleHeaderRow(headerStyle);
headerStyle.set();
row++;
@@ -315,7 +317,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
writeRecord(record);
StyleSetter totalsRowStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
- excelStylerInterface.styleTotalsRow(totalsRowStyle);
+ fastExcelStylerInterface.styleTotalsRow(totalsRowStyle);
totalsRowStyle.set();
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/FastExcelStylerInterface.java
similarity index 92%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/FastExcelStylerInterface.java
index ed58aba3..68b361eb 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/FastExcelStylerInterface.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2022. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
import org.dhatim.fastexcel.StyleSetter;
@@ -29,7 +29,7 @@ import org.dhatim.fastexcel.StyleSetter;
** Interface for classes that know how to apply styles to an Excel stream being
** built by fastexcel.
*******************************************************************************/
-public interface ExcelStylerInterface
+public interface FastExcelStylerInterface
{
/*******************************************************************************
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/PlainFastExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/PlainFastExcelStyler.java
new file mode 100644
index 00000000..e42afe36
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/fastexcel/PlainFastExcelStyler.java
@@ -0,0 +1,43 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
+
+
+import org.dhatim.fastexcel.StyleSetter;
+
+
+/*******************************************************************************
+ ** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
+ *******************************************************************************/
+public class PlainFastExcelStyler implements FastExcelStylerInterface
+{
+
+ /*******************************************************************************
+ ** ... sorry, but adding this gives us test coverage on this class, even though
+ ** we're just deferring to super...
+ *******************************************************************************/
+ @Override
+ public void styleHeaderRow(StyleSetter headerRowStyle)
+ {
+ FastExcelStylerInterface.super.styleHeaderRow(headerRowStyle);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/BoldHeaderAndFooterPoiExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/BoldHeaderAndFooterPoiExcelStyler.java
new file mode 100644
index 00000000..c30beef1
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/BoldHeaderAndFooterPoiExcelStyler.java
@@ -0,0 +1,93 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
+
+
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+
+/*******************************************************************************
+ ** Version of POI excel styler that does bold headers and footers, with basic borders.
+ *******************************************************************************/
+public class BoldHeaderAndFooterPoiExcelStyler implements PoiExcelStylerInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public XSSFCellStyle createStyleForTitle(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ Font font = workbook.createFont();
+ font.setFontHeightInPoints((short) 14);
+ font.setBold(true);
+
+ XSSFCellStyle cellStyle = workbook.createCellStyle();
+ cellStyle.setFont(font);
+ cellStyle.setAlignment(HorizontalAlignment.CENTER);
+
+ return (cellStyle);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ Font font = workbook.createFont();
+ font.setBold(true);
+
+ XSSFCellStyle cellStyle = workbook.createCellStyle();
+ cellStyle.setFont(font);
+ cellStyle.setBorderBottom(BorderStyle.THIN);
+
+ return (cellStyle);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public XSSFCellStyle createStyleForFooter(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ Font font = workbook.createFont();
+ font.setBold(true);
+
+ XSSFCellStyle cellStyle = workbook.createCellStyle();
+ cellStyle.setFont(font);
+ cellStyle.setBorderTop(BorderStyle.THIN);
+ cellStyle.setBorderBottom(BorderStyle.DOUBLE);
+
+ return (cellStyle);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingExportStreamer.java
new file mode 100644
index 00000000..7033fd62
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/ExcelPoiBasedStreamingExportStreamer.java
@@ -0,0 +1,822 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
+
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Serializable;
+import java.io.Writer;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
+import com.kingsrook.qqq.backend.core.actions.reporting.ReportUtils;
+import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
+import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue;
+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.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import org.apache.poi.ss.SpreadsheetVersion;
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.ss.usermodel.DataConsolidateFunction;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.util.AreaReference;
+import org.apache.poi.ss.util.CellReference;
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.apache.poi.xssf.usermodel.XSSFCell;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFPivotTable;
+import org.apache.poi.xssf.usermodel.XSSFRow;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+
+/*******************************************************************************
+ ** Excel export format implementation using POI library, but with modifications
+ ** to actually stream output rather than use any temp files.
+ **
+ ** For a rough outline:
+ ** - create a basically empty Excel workbook using POI - empty meaning, without
+ ** data rows.
+ ** - have POI write that workbook out into a byte[] - which will be a zip full
+ ** of xml (e.g., xlsx).
+ ** - then open a new ZipOutputStream wrapper around the OutputStream we took in
+ ** as the report destination (e.g., streamed into storage or http output)
+ ** - Copy over all entries from the xlsx into our new zip-output-stream, other than
+ ** ones that are the actual sheets that we want to put data into.
+ ** - For the sheet entries, use the StreamedPoiSheetWriter class to write the
+ ** report's data directly out as Excel XML (not using POI).
+ ** - Pivot tables require a bit of an additional hack - to write a "pivot cache
+ ** definition", which, while we won't put all the data in it (because we'll tell
+ ** it refreshOnLoad="true"), we will at least need to know how many cols & rows
+ ** are in the data-sheet (which we wouldn't know until we streamed that sheet!)
+ *******************************************************************************/
+public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInterface
+{
+ private static final QLogger LOG = QLogger.getLogger(ExcelPoiBasedStreamingExportStreamer.class);
+
+ private List views;
+ private ExportInput exportInput;
+ private List fields;
+ private OutputStream outputStream;
+ private ZipOutputStream zipOutputStream;
+
+ public static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd";
+ public static final String EXCEL_DATE_TIME_FORMAT = "yyyy-MM-dd H:mm:ss";
+
+ private PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
+ private Map excelCellFormats;
+ private Map styles = new HashMap<>();
+
+ private int rowNo = 0;
+ private int sheetIndex = 1;
+
+ private Map pivotViewToCacheDefinitionReferenceMap = new HashMap<>();
+
+ private Writer activeSheetWriter = null;
+ private StreamedSheetWriter sheetWriter = null;
+
+ private QReportView currentView = null;
+ private Map> fieldsPerView = new HashMap<>();
+ private Map rowsPerView = new HashMap<>();
+ private Map labelViewsByName = new HashMap<>();
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public ExcelPoiBasedStreamingExportStreamer()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void preRun(ReportDestination reportDestination, List views) throws QReportingException
+ {
+ try
+ {
+ this.outputStream = reportDestination.getReportOutputStream();
+ this.views = views;
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // create 'template' workbook through poi - with sheets corresponding to our //
+ // actual file this will be a zip file (stream), with entries for all of the //
+ // files in the final xlsx but without any data, so it'll be small //
+ ///////////////////////////////////////////////////////////////////////////////
+ XSSFWorkbook workbook = new XSSFWorkbook();
+ createStyles(workbook);
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ // for each of the sheets, create it in the workbook, and put a reference to it in the sheetMap //
+ //////////////////////////////////////////////////////////////////////////////////////////////////
+ Map sheetMapByExcelReference = new HashMap<>();
+ Map sheetMapByViewName = new HashMap<>();
+
+ int sheetCounter = 1;
+ for(QReportView view : views)
+ {
+ String label = Objects.requireNonNullElse(view.getLabel(), "Sheet " + sheetCounter);
+ label = WorkbookUtil.createSafeSheetName(label);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // track the actually-used sheet labels (needed for referencing in pivot table generation) //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ labelViewsByName.put(view.getName(), label);
+
+ XSSFSheet sheet = workbook.createSheet(label);
+ String sheetReference = sheet.getPackagePart().getPartName().getName().substring(1);
+ sheetMapByExcelReference.put(sheetReference, sheet);
+ sheetMapByViewName.put(view.getName(), sheet);
+ sheetCounter++;
+ }
+
+ ////////////////////////////////////////////////////
+ // if any views are pivot tables, create them now //
+ ////////////////////////////////////////////////////
+ List pivotViewNames = new ArrayList<>();
+ for(QReportView view : views)
+ {
+ if(ReportType.PIVOT.equals(view.getType()))
+ {
+ pivotViewNames.add(view.getName());
+
+ XSSFSheet pivotTableSheet = Objects.requireNonNull(sheetMapByViewName.get(view.getName()), "Could not get pivot table sheet view by name: " + view.getName());
+ XSSFSheet dataSheet = Objects.requireNonNull(sheetMapByViewName.get(view.getPivotTableSourceViewName()), "Could not get pivot table source sheet by view name: " + view.getPivotTableSourceViewName());
+ QReportView dataView = ReportUtils.getSourceViewForPivotTableView(views, view);
+ createPivotTableTemplate(pivotTableSheet, view, dataSheet, dataView);
+ }
+ }
+ Iterator pivotViewNameIterator = pivotViewNames.iterator();
+
+ /////////////////////////////////////////////////////////
+ // write that template worksheet zip out to byte array //
+ /////////////////////////////////////////////////////////
+ ByteArrayOutputStream templateBAOS = new ByteArrayOutputStream();
+ workbook.write(templateBAOS);
+ templateBAOS.close();
+ byte[] templateBytes = templateBAOS.toByteArray();
+
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ // open up a zipOutputStream around the output stream that the report is to be written to. //
+ /////////////////////////////////////////////////////////////////////////////////////////////
+ this.zipOutputStream = new ZipOutputStream(this.outputStream);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // copy over all the entries in the template zip that aren't the sheets into the output stream //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(templateBytes));
+ ZipEntry zipTemplateEntry = null;
+ byte[] buffer = new byte[2048];
+ while((zipTemplateEntry = zipInputStream.getNextEntry()) != null)
+ {
+ if(zipTemplateEntry.getName().matches(".*/pivotCacheDefinition.*.xml"))
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if this zip entry is a pivotCacheDefinition, then don't write it to the output stream right now. //
+ // instead, just map the pivot view's name to the zipTemplateEntry name //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(!pivotViewNameIterator.hasNext())
+ {
+ throw new QReportingException("Found a pivot cache definition [" + zipTemplateEntry.getName() + "] in the template ZIP, but no (more) corresponding pivot view names");
+ }
+
+ String pivotViewName = pivotViewNameIterator.next();
+ LOG.info("Holding on a pivot cache definition zip template entry [" + pivotViewName + "] [" + zipTemplateEntry.getName() + "]...");
+ pivotViewToCacheDefinitionReferenceMap.put(pivotViewName, zipTemplateEntry.getName());
+ }
+ else if(!sheetMapByExcelReference.containsKey(zipTemplateEntry.getName()))
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // else, if we don't have this zipTemplateEntry name in our map of sheets, then this is a kinda "meta" //
+ // file that we don't really care about (e.g., not our sheet data), so just copy it to the output stream. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ LOG.info("Copying zip template entry [" + zipTemplateEntry.getName() + "] to output stream");
+ zipOutputStream.putNextEntry(new ZipEntry(zipTemplateEntry.getName()));
+
+ int length;
+ while((length = zipInputStream.read(buffer)) > 0)
+ {
+ zipOutputStream.write(buffer, 0, length);
+ }
+
+ zipInputStream.closeEntry();
+ }
+ else
+ {
+ ////////////////////////////////////////////////////////////////////////////////////
+ // else - this is a sheet - so again, don't write it yet - stream its data below. //
+ ////////////////////////////////////////////////////////////////////////////////////
+ LOG.info("Skipping presumed sheet zip template entry [" + zipTemplateEntry.getName() + "] to output stream");
+ }
+ }
+
+ zipInputStream.close();
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error preparing to generate spreadsheet", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void createPivotTableTemplate(XSSFSheet pivotTableSheet, QReportView pivotView, XSSFSheet dataSheet, QReportView dataView) throws QReportingException
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // write just enough data to the dataView's sheet so that we can refer to it for creating the pivot table. //
+ // we need to do this, because POI will try to create the pivotCache referring to the data sheet, and if //
+ // there isn't any data there, it'll crash. //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ XSSFRow headerRow = dataSheet.createRow(0);
+ int columnNo = 0;
+ for(QReportField column : dataView.getColumns())
+ {
+ XSSFCell cell = headerRow.createCell(columnNo++);
+ cell.setCellValue(QInstanceEnricher.nameToLabel(column.getName()));
+ }
+
+ XSSFRow valuesRow = dataSheet.createRow(1);
+ columnNo = 0;
+ for(QReportField column : dataView.getColumns())
+ {
+ XSSFCell cell = valuesRow.createCell(columnNo++);
+ cell.setCellValue("Value " + columnNo);
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ // for this template version of the pivot table, tell it there are only 2 rows in the source sheet //
+ // as that's all that we wrote above (a header and 1 fake value row) //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////
+ int rows = 2;
+ String colsLetter = CellReference.convertNumToColString(dataView.getColumns().size() - 1);
+ AreaReference source = new AreaReference("A1:" + colsLetter + rows, SpreadsheetVersion.EXCEL2007);
+ CellReference position = new CellReference("A1");
+
+ //////////////////////////////////////////////////////////////////
+ // tell poi all about our pivot table - rows, cols, and columns //
+ //////////////////////////////////////////////////////////////////
+ XSSFPivotTable pivotTable = pivotTableSheet.createPivotTable(source, position, dataSheet);
+
+ for(PivotTableGroupBy row : CollectionUtils.nonNullList(pivotView.getPivotTableDefinition().getRows()))
+ {
+ int rowLabelColumnIndex = getColumnIndex(dataView.getColumns(), row.getFieldName());
+ pivotTable.addRowLabel(rowLabelColumnIndex);
+ }
+
+ for(PivotTableGroupBy column : CollectionUtils.nonNullList(pivotView.getPivotTableDefinition().getColumns()))
+ {
+ int colLabelColumnIndex = getColumnIndex(dataView.getColumns(), column.getFieldName());
+ pivotTable.addColLabel(colLabelColumnIndex);
+ }
+
+ for(PivotTableValue value : CollectionUtils.nonNullList(pivotView.getPivotTableDefinition().getValues()))
+ {
+ int columnLabelColumnIndex = getColumnIndex(dataView.getColumns(), value.getFieldName());
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // todo - some bug where, if use a group-by field here, then ... it doesn't get used for the grouping. //
+ // g-sheets does let me do this, so, maybe, download their file and see how it's different? //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////
+ String labelPrefix = value.getFunction().name() + " of ";
+ String label = labelPrefix + QInstanceEnricher.nameToLabel(value.getFieldName());
+ String valueFormat = null;
+
+ Optional optSourceField = dataView.getColumns().stream().filter(c -> c.getName().equals(value.getFieldName())).findFirst();
+ if(optSourceField.isPresent())
+ {
+ QReportField sourceField = optSourceField.get();
+
+ if(StringUtils.hasContent(sourceField.getLabel()))
+ {
+ label = labelPrefix + sourceField.getLabel();
+ }
+
+ if(StringUtils.hasContent(sourceField.getDisplayFormat()))
+ {
+ valueFormat = DisplayFormat.getExcelFormat(sourceField.getDisplayFormat());
+ }
+ else
+ {
+ if(QFieldType.DATE.equals(sourceField.getType()))
+ {
+ valueFormat = EXCEL_DATE_FORMAT;
+ }
+ else if(QFieldType.DATE_TIME.equals(sourceField.getType()))
+ {
+ valueFormat = EXCEL_DATE_TIME_FORMAT;
+ }
+ }
+ }
+
+ pivotTable.addColumnLabel(DataConsolidateFunction.valueOf(value.getFunction().name()), columnLabelColumnIndex, label, valueFormat);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private int getColumnIndex(List columns, String fieldName) throws QReportingException
+ {
+ for(int i = 0; i < columns.size(); i++)
+ {
+ if(columns.get(i).getName().equals(fieldName))
+ {
+ return (i);
+ }
+ }
+
+ throw (new QReportingException("Could not find column by name [" + fieldName + "]"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void createStyles(XSSFWorkbook workbook)
+ {
+ CreationHelper createHelper = workbook.getCreationHelper();
+
+ XSSFCellStyle dateStyle = workbook.createCellStyle();
+ dateStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_FORMAT));
+ styles.put("date", dateStyle);
+
+ XSSFCellStyle dateTimeStyle = workbook.createCellStyle();
+ dateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
+ styles.put("datetime", dateTimeStyle);
+
+ styles.put("title", poiExcelStylerInterface.createStyleForTitle(workbook, createHelper));
+ styles.put("header", poiExcelStylerInterface.createStyleForHeader(workbook, createHelper));
+ styles.put("footer", poiExcelStylerInterface.createStyleForFooter(workbook, createHelper));
+
+ XSSFCellStyle footerDateStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper);
+ footerDateStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_FORMAT));
+ styles.put("footer-date", footerDateStyle);
+
+ XSSFCellStyle footerDateTimeStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper);
+ footerDateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
+ styles.put("footer-datetime", footerDateTimeStyle);
+ }
+
+
+
+ /*******************************************************************************
+ ** Starts a new worksheet in the current workbook. Can be called multiple times.
+ *******************************************************************************/
+ @Override
+ public void start(ExportInput exportInput, List fields, String label, QReportView view) throws QReportingException
+ {
+ try
+ {
+ /////////////////////////////////////////
+ // close previous sheet if one is open //
+ /////////////////////////////////////////
+ closeLastSheetIfOpen();
+
+ if(currentView != null)
+ {
+ this.rowsPerView.put(currentView.getName(), rowNo);
+ }
+
+ this.currentView = view;
+ this.exportInput = exportInput;
+ this.fields = fields;
+ this.rowNo = 0;
+
+ this.fieldsPerView.put(view.getName(), fields);
+
+ //////////////////////////////////////////
+ // start the new sheet as: //
+ // - a new entry in the zipOutputStream //
+ // - with a new output stream writer //
+ // - and with a SpreadsheetWriter //
+ //////////////////////////////////////////
+ zipOutputStream.putNextEntry(new ZipEntry("xl/worksheets/sheet" + this.sheetIndex++ + ".xml"));
+ activeSheetWriter = new OutputStreamWriter(zipOutputStream);
+ sheetWriter = new StreamedSheetWriter(activeSheetWriter);
+
+ if(ReportType.PIVOT.equals(view.getType()))
+ {
+ writePivotTable(view, ReportUtils.getSourceViewForPivotTableView(views, view));
+ }
+ else
+ {
+ sheetWriter.beginSheet();
+
+ ////////////////////////////////////////////////
+ // put the title and header rows in the sheet //
+ ////////////////////////////////////////////////
+ writeTitleAndHeader();
+ }
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error starting worksheet", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void writeTitleAndHeader() throws QReportingException
+ {
+ try
+ {
+ ///////////////
+ // title row //
+ ///////////////
+ if(StringUtils.hasContent(exportInput.getTitleRow()))
+ {
+ sheetWriter.insertRow(rowNo++);
+ sheetWriter.createCell(0, exportInput.getTitleRow(), styles.get("title").getIndex());
+ sheetWriter.endRow();
+ }
+
+ ////////////////
+ // header row //
+ ////////////////
+ if(exportInput.getIncludeHeaderRow())
+ {
+ sheetWriter.insertRow(rowNo++);
+ XSSFCellStyle headerStyle = styles.get("header");
+
+ int col = 0;
+ for(QFieldMetaData column : fields)
+ {
+ sheetWriter.createCell(col, column.getLabel(), headerStyle.getIndex());
+ col++;
+ }
+
+ sheetWriter.endRow();
+ }
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error starting Excel report"));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void addRecords(List qRecords) throws QReportingException
+ {
+ LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
+
+ try
+ {
+ for(QRecord qRecord : qRecords)
+ {
+ writeRecord(qRecord);
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.error("Exception generating excel file", e);
+ try
+ {
+ outputStream.close();
+ }
+ catch(IOException ex)
+ {
+ LOG.warn("Secondary error closing excel output stream", e);
+ }
+
+ throw (new QReportingException("Error generating Excel report", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void writeRecord(QRecord qRecord) throws IOException
+ {
+ writeRecord(qRecord, false);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void writeRecord(QRecord qRecord, boolean isFooter) throws IOException
+ {
+ sheetWriter.insertRow(rowNo++);
+
+ int styleIndex = -1;
+ int dateStyleIndex = styles.get("date").getIndex();
+ int dateTimeStyleIndex = styles.get("datetime").getIndex();
+ if(isFooter)
+ {
+ styleIndex = styles.get("footer").getIndex();
+ dateStyleIndex = styles.get("footer-date").getIndex();
+ dateTimeStyleIndex = styles.get("footer-datetime").getIndex();
+ }
+
+ int col = 0;
+ for(QFieldMetaData field : fields)
+ {
+ Serializable value = qRecord.getValue(field.getName());
+
+ if(value != null)
+ {
+ if(value instanceof String s)
+ {
+ sheetWriter.createCell(col, s, styleIndex);
+ }
+ else if(value instanceof Number n)
+ {
+ sheetWriter.createCell(col, n.doubleValue(), styleIndex);
+
+ if(excelCellFormats != null)
+ {
+ String format = excelCellFormats.get(field.getName());
+ if(format != null)
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // todo - so - for this streamed/zip approach, we need to know all styles before we start //
+ // any sheets. but, right now Report action only calls us with per-sheet styles when //
+ // it's starting individual sheets. so, we can't quite support this at this time. //
+ // "just" need to change GenerateReportAction to look up all cell formats for all sheets //
+ // before preRun is called... and change all existing streamer classes to handle that too //
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // worksheet.style(rowNo, col).format(format).set();
+ }
+ }
+ }
+ else if(value instanceof Boolean b)
+ {
+ sheetWriter.createCell(col, b, styleIndex);
+ }
+ else if(value instanceof Date d)
+ {
+ sheetWriter.createCell(col, DateUtil.getExcelDate(d), dateStyleIndex);
+ }
+ else if(value instanceof LocalDate d)
+ {
+ sheetWriter.createCell(col, DateUtil.getExcelDate(d), dateStyleIndex);
+ }
+ else if(value instanceof LocalDateTime d)
+ {
+ sheetWriter.createCell(col, DateUtil.getExcelDate(d), dateStyleIndex);
+ }
+ else if(value instanceof ZonedDateTime d)
+ {
+ sheetWriter.createCell(col, DateUtil.getExcelDate(d.toLocalDateTime()), dateTimeStyleIndex);
+ }
+ else if(value instanceof Instant i)
+ {
+ sheetWriter.createCell(col, DateUtil.getExcelDate(i.atZone(ZoneId.systemDefault()).toLocalDateTime()), dateTimeStyleIndex);
+ }
+ else
+ {
+ sheetWriter.createCell(col, ValueUtils.getValueAsString(value), styleIndex);
+ }
+ }
+
+ col++;
+ }
+
+ sheetWriter.endRow();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void addTotalsRow(QRecord record) throws QReportingException
+ {
+ try
+ {
+ writeRecord(record, true);
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error adding totals row", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void finish() throws QReportingException
+ {
+ try
+ {
+ //////////////////////////////////////////////
+ // close the last open sheet if one is open //
+ //////////////////////////////////////////////
+ closeLastSheetIfOpen();
+
+ /////////////////////////////////////////////////////////////////////////////////////
+ // so, we DO need a close here, on the zipOutputStream, to finish its "zippiness". //
+ // even though, doing so also closes the outputStream from the caller that this //
+ // zipOutputStream is wrapping (and the caller will likely call close on too)... //
+ /////////////////////////////////////////////////////////////////////////////////////
+ zipOutputStream.close();
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error finishing Excel report", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void closeLastSheetIfOpen() throws IOException
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if we have an active sheet writer: //
+ // - end the current sheet in the spreadsheet writer (write some closing xml, unless it's a pivot!) //
+ // - flush the contents through the activeSheetWriter //
+ // - close the zip entry in the output stream. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(activeSheetWriter != null)
+ {
+ if(!ReportType.PIVOT.equals(currentView.getType()))
+ {
+ sheetWriter.endSheet();
+ }
+
+ activeSheetWriter.flush();
+ zipOutputStream.closeEntry();
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** display formats is a map of field name to Excel format strings (e.g., $#,##0.00)
+ *******************************************************************************/
+ @Override
+ public void setDisplayFormats(Map displayFormats)
+ {
+ this.excelCellFormats = new HashMap<>();
+ for(Map.Entry entry : displayFormats.entrySet())
+ {
+ String excelFormat = DisplayFormat.getExcelFormat(entry.getValue());
+ if(excelFormat != null)
+ {
+ excelCellFormats.put(entry.getKey(), excelFormat);
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void writePivotTable(QReportView pivotTableView, QReportView dataView) throws QReportingException
+ {
+ try
+ {
+ //////////////////////////////////////////////////////////////////////////////////
+ // write the xml file that is the pivot table sheet. //
+ // note that the ZipEntry here will have been started above in the start method //
+ //////////////////////////////////////////////////////////////////////////////////
+ activeSheetWriter.write("""
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """);
+ activeSheetWriter.flush();
+
+ ////////////////////////////////////////////////////////////////////////////
+ // start a new zip entry, for this pivot view's cacheDefinition reference //
+ ////////////////////////////////////////////////////////////////////////////
+ zipOutputStream.putNextEntry(new ZipEntry(pivotViewToCacheDefinitionReferenceMap.get(pivotTableView.getName())));
+
+ /////////////////////////////////////////////////////////
+ // prepare the xml for each field (e.g., w/ its label) //
+ /////////////////////////////////////////////////////////
+ List cachedFieldElements = new ArrayList<>();
+ for(QFieldMetaData column : this.fieldsPerView.get(dataView.getName()))
+ {
+ cachedFieldElements.add(String.format("""
+
+
+
+ """, column.getLabel()));
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////
+ // write the xml file that is the pivot cache definition (structure only, no data) //
+ /////////////////////////////////////////////////////////////////////////////////////
+ activeSheetWriter = new OutputStreamWriter(zipOutputStream);
+ activeSheetWriter.write(String.format("""
+
+
+
+
+
+
+ %s
+
+
+ """,
+ StreamedSheetWriter.cleanseValue(labelViewsByName.get(dataView.getName())),
+ CellReference.convertNumToColString(dataView.getColumns().size() - 1),
+ rowsPerView.get(dataView.getName()),
+ dataView.getColumns().size(),
+ StringUtils.join("\n", cachedFieldElements)));
+ }
+ catch(Exception e)
+ {
+ throw (new QReportingException("Error writing pivot table", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected PoiExcelStylerInterface getStylerInterface()
+ {
+ return (new PlainPoiExcelStyler());
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PlainPoiExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PlainPoiExcelStyler.java
new file mode 100644
index 00000000..15f43ee0
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PlainPoiExcelStyler.java
@@ -0,0 +1,46 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
+
+
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+
+/*******************************************************************************
+ ** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
+ *******************************************************************************/
+public class PlainPoiExcelStyler implements PoiExcelStylerInterface
+{
+
+ /*******************************************************************************
+ ** ... sorry, but adding this gives us test coverage on this class, even though
+ ** we're just deferring to super...
+ *******************************************************************************/
+ @Override
+ public XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ return PoiExcelStylerInterface.super.createStyleForHeader(workbook, createHelper);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PoiExcelStylerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PoiExcelStylerInterface.java
new file mode 100644
index 00000000..c052e194
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/PoiExcelStylerInterface.java
@@ -0,0 +1,59 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
+
+
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+
+/*******************************************************************************
+ ** Interface for classes that know how to apply styles to an Excel stream being
+ ** built by POI.
+ *******************************************************************************/
+public interface PoiExcelStylerInterface
+{
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default XSSFCellStyle createStyleForTitle(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ return (workbook.createCellStyle());
+ }
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ return (workbook.createCellStyle());
+ }
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default XSSFCellStyle createStyleForFooter(XSSFWorkbook workbook, CreationHelper createHelper)
+ {
+ return (workbook.createCellStyle());
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedSheetWriter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedSheetWriter.java
new file mode 100644
index 00000000..850a1ecc
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excel/poi/StreamedSheetWriter.java
@@ -0,0 +1,242 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
+
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.poi.ss.util.CellReference;
+
+
+/*******************************************************************************
+ ** Write excel formatted XML to a Writer.
+ ** Originally from https://coderanch.com/t/548897/java/Generate-large-excel-POI
+ *******************************************************************************/
+public class StreamedSheetWriter
+{
+ private final Writer writer;
+ private int rowNo;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public StreamedSheetWriter(Writer writer)
+ {
+ this.writer = writer;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void beginSheet() throws IOException
+ {
+ writer.write("""
+
+
+ """);
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void endSheet() throws IOException
+ {
+ writer.write("""
+
+ """);
+ }
+
+
+
+ /*******************************************************************************
+ ** Insert a new row
+ **
+ ** @param rowNo 0-based row number
+ *******************************************************************************/
+ public void insertRow(int rowNo) throws IOException
+ {
+ writer.write("\n");
+ this.rowNo = rowNo;
+ }
+
+
+
+ /*******************************************************************************
+ ** Insert row end marker
+ *******************************************************************************/
+ public void endRow() throws IOException
+ {
+ writer.write("
\n");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, String value, int styleIndex) throws IOException
+ {
+ String ref = new CellReference(rowNo, columnIndex).formatAsString();
+ writer.write("");
+ writer.write("" + cleanValue + "");
+ writer.write("");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static String cleanseValue(String value)
+ {
+ if(value != null)
+ {
+ StringBuilder rs = new StringBuilder();
+ for(int i = 0; i < value.length(); i++)
+ {
+ char c = value.charAt(i);
+ if(c == '&')
+ {
+ rs.append("&");
+ }
+ else if(c == '<')
+ {
+ rs.append("<");
+ }
+ else if(c == '>')
+ {
+ rs.append(">");
+ }
+ else if(c == '\'')
+ {
+ rs.append("'");
+ }
+ else if(c == '"')
+ {
+ rs.append(""");
+ }
+ else if (c < 32 && c != '\t' && c != '\n')
+ {
+ rs.append(' ');
+ }
+ else
+ {
+ rs.append(c);
+ }
+ }
+
+ Map m = new HashMap();
+ m.computeIfAbsent("s", (s) -> 3);
+
+ value = rs.toString();
+ }
+
+ return (value);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, String value) throws IOException
+ {
+ createCell(columnIndex, value, -1);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, double value, int styleIndex) throws IOException
+ {
+ String ref = new CellReference(rowNo, columnIndex).formatAsString();
+ writer.write("");
+ writer.write("" + value + "");
+ writer.write("");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, double value) throws IOException
+ {
+ createCell(columnIndex, value, -1);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, Boolean value) throws IOException
+ {
+ createCell(columnIndex, value, -1);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void createCell(int columnIndex, Boolean value, int styleIndex) throws IOException
+ {
+ String ref = new CellReference(rowNo, columnIndex).formatAsString();
+ writer.write("");
+ if(value != null)
+ {
+ writer.write("" + (value ? 1 : 0) + "");
+ }
+ writer.write("");
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/StorageAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/StorageAction.java
new file mode 100644
index 00000000..6de52d99
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/StorageAction.java
@@ -0,0 +1,96 @@
+/*
+ * 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.tables;
+
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+import com.kingsrook.qqq.backend.core.actions.interfaces.QStorageInterface;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
+import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
+
+
+/*******************************************************************************
+ ** Action to do (generally, "mass") storage operations in a backend.
+ **
+ ** e.g., store a (potentially large) file - specifically - by working with it
+ ** as either an InputStream or OutputStream.
+ **
+ ** May not be implemented in all backends.
+ **
+ *******************************************************************************/
+public class StorageAction
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public OutputStream createOutputStream(StorageInput storageInput) throws QException
+ {
+ QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
+ QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
+ return (storageInterface.createOutputStream(storageInput));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public InputStream getInputStream(StorageInput storageInput) throws QException
+ {
+ QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
+ QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
+ return (storageInterface.getInputStream(storageInput));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QBackendModuleInterface preAction(StorageInput storageInput) throws QException
+ {
+ ActionHelper.validateSession(storageInput);
+
+ if(storageInput.getTableName() == null)
+ {
+ throw (new QException("Table name was not specified in storage input"));
+ }
+
+ QTableMetaData table = storageInput.getTable();
+ if(table == null)
+ {
+ throw (new QException("A table named [" + storageInput.getTableName() + "] was not found in the active QInstance"));
+ }
+
+ QBackendMetaData backend = storageInput.getBackend();
+ QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
+ QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
+ return (qModule);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceHelpContentManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceHelpContentManager.java
index f0e6968c..b0e618ce 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceHelpContentManager.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceHelpContentManager.java
@@ -149,8 +149,7 @@ public class QInstanceHelpContentManager
}
else if(StringUtils.hasContent(widgetName))
{
- processHelpContentForWidget(key, widgetName, slotName, helpContent);
-
+ processHelpContentForWidget(key, widgetName, slotName, roles, helpContent);
}
}
catch(Exception e)
@@ -252,7 +251,7 @@ public class QInstanceHelpContentManager
/*******************************************************************************
**
*******************************************************************************/
- private static void processHelpContentForWidget(String key, String widgetName, String slotName, QHelpContent helpContent)
+ private static void processHelpContentForWidget(String key, String widgetName, String slotName, Set roles, QHelpContent helpContent)
{
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
if(!StringUtils.hasContent(slotName))
@@ -265,22 +264,14 @@ public class QInstanceHelpContentManager
}
else
{
- Map widgetHelpContent = widget.getHelpContent();
- if(widgetHelpContent == null)
- {
- widgetHelpContent = new HashMap<>();
- }
-
if(helpContent != null)
{
- widgetHelpContent.put(slotName, helpContent);
+ widget.withHelpContent(slotName, helpContent);
}
else
{
- widgetHelpContent.remove(slotName);
+ widget.removeHelpContent(slotName, roles);
}
-
- widget.setHelpContent(widgetHelpContent);
}
}
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 65acd19d..85c7e307 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
@@ -1383,12 +1383,17 @@ public class QInstanceValidator
///////////////////////////////////
// validate steps in the process //
///////////////////////////////////
+ Set usedStepNames = new HashSet<>();
if(assertCondition(CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + "."))
{
int index = 0;
for(QStepMetaData step : process.getStepList())
{
- assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName);
+ if(assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName))
+ {
+ assertCondition(!usedStepNames.contains(step.getName()), "Duplicate step name [" + step.getName() + "] in process " + processName);
+ usedStepNames.add(step.getName());
+ }
index++;
////////////////////////////////////////////
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
index b774ed73..2ed0ba4e 100644
--- 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
@@ -22,7 +22,6 @@
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;
@@ -37,9 +36,8 @@ public class ExportInput extends AbstractTableActionInput
private Integer limit;
private List fieldNames;
- private String filename;
- private ReportFormat reportFormat;
- private OutputStream reportOutputStream;
+ private ReportDestination reportDestination;
+
private String titleRow;
private boolean includeHeaderRow = true;
@@ -120,71 +118,6 @@ public class ExportInput extends AbstractTableActionInput
- /*******************************************************************************
- ** 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;
- }
-
-
/*******************************************************************************
**
@@ -226,4 +159,36 @@ public class ExportInput extends AbstractTableActionInput
this.includeHeaderRow = includeHeaderRow;
}
+
+
+ /*******************************************************************************
+ ** Getter for reportDestination
+ *******************************************************************************/
+ public ReportDestination getReportDestination()
+ {
+ return (this.reportDestination);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for reportDestination
+ *******************************************************************************/
+ public void setReportDestination(ReportDestination reportDestination)
+ {
+ this.reportDestination = reportDestination;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for reportDestination
+ *******************************************************************************/
+ public ExportInput withReportDestination(ReportDestination reportDestination)
+ {
+ this.reportDestination = reportDestination;
+ return (this);
+ }
+
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportDestination.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportDestination.java
new file mode 100644
index 00000000..decc8c5f
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportDestination.java
@@ -0,0 +1,131 @@
+/*
+ * 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.model.actions.reporting;
+
+
+import java.io.OutputStream;
+
+
+/*******************************************************************************
+ ** Member of report & export Inputs, that wraps details about the destination of
+ ** where & how the report (or export) is being written.
+ *******************************************************************************/
+public class ReportDestination
+{
+ private String filename;
+ private ReportFormat reportFormat;
+ private OutputStream reportOutputStream;
+
+
+
+ /*******************************************************************************
+ ** Getter for filename
+ *******************************************************************************/
+ public String getFilename()
+ {
+ return (this.filename);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for filename
+ *******************************************************************************/
+ public void setFilename(String filename)
+ {
+ this.filename = filename;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for filename
+ *******************************************************************************/
+ public ReportDestination withFilename(String filename)
+ {
+ this.filename = filename;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for reportFormat
+ *******************************************************************************/
+ public ReportFormat getReportFormat()
+ {
+ return (this.reportFormat);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for reportFormat
+ *******************************************************************************/
+ public void setReportFormat(ReportFormat reportFormat)
+ {
+ this.reportFormat = reportFormat;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for reportFormat
+ *******************************************************************************/
+ public ReportDestination withReportFormat(ReportFormat reportFormat)
+ {
+ this.reportFormat = reportFormat;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for reportOutputStream
+ *******************************************************************************/
+ public OutputStream getReportOutputStream()
+ {
+ return (this.reportOutputStream);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for reportOutputStream
+ *******************************************************************************/
+ public void setReportOutputStream(OutputStream reportOutputStream)
+ {
+ this.reportOutputStream = reportOutputStream;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for reportOutputStream
+ *******************************************************************************/
+ public ReportDestination withReportOutputStream(OutputStream reportOutputStream)
+ {
+ this.reportOutputStream = reportOutputStream;
+ return (this);
+ }
+
+}
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 c95b1be8..c72056f4 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
@@ -25,13 +25,13 @@ 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.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.JsonExportStreamer;
import com.kingsrook.qqq.backend.core.actions.reporting.ListOfMapsExportStreamer;
+import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.ExcelPoiBasedStreamingExportStreamer;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
-import org.dhatim.fastexcel.Worksheet;
+import org.apache.poi.ss.SpreadsheetVersion;
/*******************************************************************************
@@ -39,15 +39,24 @@ import org.dhatim.fastexcel.Worksheet;
*******************************************************************************/
public enum ReportFormat
{
- XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
- JSON(null, null, JsonExportStreamer::new, "application/json"),
- CSV(null, null, CsvExportStreamer::new, "text/csv"),
- LIST_OF_MAPS(null, null, ListOfMapsExportStreamer::new, null);
+ /////////////////////////////////////////////////////////////////////////
+ // if we need to fall back to Fastexcel, this was its version of this. //
+ /////////////////////////////////////////////////////////////////////////
+ // XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelFastexcelExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx", true, false, true),
+
+ XLSX(SpreadsheetVersion.EXCEL2007.getMaxRows(), SpreadsheetVersion.EXCEL2007.getMaxColumns(), ExcelPoiBasedStreamingExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx", true, true, true),
+ JSON(null, null, JsonExportStreamer::new, "application/json", "json", false, false, true),
+ CSV(null, null, CsvExportStreamer::new, "text/csv", "csv", false, false, false),
+ LIST_OF_MAPS(null, null, ListOfMapsExportStreamer::new, null, null, false, false, true);
private final Integer maxRows;
private final Integer maxCols;
private final String mimeType;
+ private final String extension;
+ private final boolean isBinary;
+ private final boolean supportsNativePivotTables;
+ private final boolean supportsMultipleViews;
private final Supplier extends ExportStreamerInterface> streamerConstructor;
@@ -56,12 +65,16 @@ public enum ReportFormat
/*******************************************************************************
**
*******************************************************************************/
- ReportFormat(Integer maxRows, Integer maxCols, Supplier extends ExportStreamerInterface> streamerConstructor, String mimeType)
+ ReportFormat(Integer maxRows, Integer maxCols, Supplier extends ExportStreamerInterface> streamerConstructor, String mimeType, String extension, boolean isBinary, boolean supportsNativePivotTables, boolean supportsMultipleViews)
{
this.maxRows = maxRows;
this.maxCols = maxCols;
this.mimeType = mimeType;
this.streamerConstructor = streamerConstructor;
+ this.extension = extension;
+ this.isBinary = isBinary;
+ this.supportsNativePivotTables = supportsNativePivotTables;
+ this.supportsMultipleViews = supportsMultipleViews;
}
@@ -128,4 +141,48 @@ public enum ReportFormat
{
return (streamerConstructor.get());
}
+
+
+
+ /*******************************************************************************
+ ** Getter for extension
+ **
+ *******************************************************************************/
+ public String getExtension()
+ {
+ return extension;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for isBinary
+ **
+ *******************************************************************************/
+ public boolean getIsBinary()
+ {
+ return isBinary;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for supportsNativePivotTables
+ **
+ *******************************************************************************/
+ public boolean getSupportsNativePivotTables()
+ {
+ return supportsNativePivotTables;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for supportsMultipleViews
+ **
+ *******************************************************************************/
+ public boolean getSupportsMultipleViews()
+ {
+ return supportsMultipleViews;
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportFormatPossibleValueEnum.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportFormatPossibleValueEnum.java
new file mode 100644
index 00000000..4dec5da9
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportFormatPossibleValueEnum.java
@@ -0,0 +1,59 @@
+/*
+ * 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 com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
+
+
+/*******************************************************************************
+ ** sub-set of ReportFormats to expose as possible-values in-apps
+ *******************************************************************************/
+public enum ReportFormatPossibleValueEnum implements PossibleValueEnum
+{
+ XLSX,
+ CSV,
+ JSON;
+
+ public static final String NAME = "reportFormat";
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getPossibleValueId()
+ {
+ return name();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getPossibleValueLabel()
+ {
+ return name();
+ }
+}
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 e712a79e..be901e94 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
@@ -22,24 +22,28 @@
package com.kingsrook.qqq.backend.core.model.actions.reporting;
-import java.io.OutputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
+import java.util.function.Supplier;
+import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
/*******************************************************************************
- ** Input for an Export action
+ ** Input for a Report action
*******************************************************************************/
public class ReportInput extends AbstractTableActionInput
{
- private String reportName;
+ private String reportName;
+ private QReportMetaData reportMetaData;
+
private Map inputValues;
- private String filename;
- private ReportFormat reportFormat;
- private OutputStream reportOutputStream;
+ private ReportDestination reportDestination;
+
+ private Supplier extends ExportStreamerInterface> overrideExportStreamerSupplier;
@@ -111,66 +115,97 @@ public class ReportInput extends AbstractTableActionInput
/*******************************************************************************
- ** Getter for filename
- **
+ ** Getter for reportDestination
*******************************************************************************/
- public String getFilename()
+ public ReportDestination getReportDestination()
{
- return filename;
+ return (this.reportDestination);
}
/*******************************************************************************
- ** Setter for filename
- **
+ ** Setter for reportDestination
*******************************************************************************/
- public void setFilename(String filename)
+ public void setReportDestination(ReportDestination reportDestination)
{
- this.filename = filename;
+ this.reportDestination = reportDestination;
}
/*******************************************************************************
- ** Getter for reportFormat
- **
+ ** Fluent setter for reportDestination
*******************************************************************************/
- public ReportFormat getReportFormat()
+ public ReportInput withReportDestination(ReportDestination reportDestination)
{
- return reportFormat;
+ this.reportDestination = reportDestination;
+ return (this);
}
/*******************************************************************************
- ** Setter for reportFormat
- **
+ ** Getter for reportMetaData
*******************************************************************************/
- public void setReportFormat(ReportFormat reportFormat)
+ public QReportMetaData getReportMetaData()
{
- this.reportFormat = reportFormat;
+ return (this.reportMetaData);
}
/*******************************************************************************
- ** Getter for reportOutputStream
- **
+ ** Setter for reportMetaData
*******************************************************************************/
- public OutputStream getReportOutputStream()
+ public void setReportMetaData(QReportMetaData reportMetaData)
{
- return reportOutputStream;
+ this.reportMetaData = reportMetaData;
}
/*******************************************************************************
- ** Setter for reportOutputStream
+ ** Fluent setter for reportMetaData
+ *******************************************************************************/
+ public ReportInput withReportMetaData(QReportMetaData reportMetaData)
+ {
+ this.reportMetaData = reportMetaData;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for overrideExportStreamerSupplier
**
*******************************************************************************/
- public void setReportOutputStream(OutputStream reportOutputStream)
+ public Supplier extends ExportStreamerInterface> getOverrideExportStreamerSupplier()
{
- this.reportOutputStream = reportOutputStream;
+ return overrideExportStreamerSupplier;
}
+
+
+
+ /*******************************************************************************
+ ** Setter for overrideExportStreamerSupplier
+ **
+ *******************************************************************************/
+ public void setOverrideExportStreamerSupplier(Supplier extends ExportStreamerInterface> overrideExportStreamerSupplier)
+ {
+ this.overrideExportStreamerSupplier = overrideExportStreamerSupplier;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for overrideExportStreamerSupplier
+ **
+ *******************************************************************************/
+ public ReportInput withOverrideExportStreamerSupplier(Supplier extends ExportStreamerInterface> overrideExportStreamerSupplier)
+ {
+ this.overrideExportStreamerSupplier = overrideExportStreamerSupplier;
+ return (this);
+ }
+
}
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/ReportOutput.java
new file mode 100644
index 00000000..7c51ad8e
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/ReportOutput.java
@@ -0,0 +1,67 @@
+/*
+ * 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.Serializable;
+import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
+
+
+/*******************************************************************************
+ ** Output for a Report action
+ *******************************************************************************/
+public class ReportOutput extends AbstractActionOutput implements Serializable
+{
+ private Integer totalRecordCount;
+
+
+
+ /*******************************************************************************
+ ** Getter for totalRecordCount
+ *******************************************************************************/
+ public Integer getTotalRecordCount()
+ {
+ return (this.totalRecordCount);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for totalRecordCount
+ *******************************************************************************/
+ public void setTotalRecordCount(Integer totalRecordCount)
+ {
+ this.totalRecordCount = totalRecordCount;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for totalRecordCount
+ *******************************************************************************/
+ public ReportOutput withTotalRecordCount(Integer totalRecordCount)
+ {
+ this.totalRecordCount = totalRecordCount;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableDefinition.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableDefinition.java
new file mode 100644
index 00000000..991597d3
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableDefinition.java
@@ -0,0 +1,217 @@
+/*
+ * 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.model.actions.reporting.pivottable;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/*******************************************************************************
+ ** Full definition of a pivot table - its rows, columns, and values.
+ *******************************************************************************/
+public class PivotTableDefinition implements Cloneable, Serializable
+{
+ private List rows;
+ private List columns;
+ private List values;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ protected PivotTableDefinition clone() throws CloneNotSupportedException
+ {
+ PivotTableDefinition clone = (PivotTableDefinition) super.clone();
+
+ if(rows != null)
+ {
+ clone.rows = new ArrayList<>();
+ for(PivotTableGroupBy row : rows)
+ {
+ clone.rows.add(row.clone());
+ }
+ }
+
+ if(columns != null)
+ {
+ clone.columns = new ArrayList<>();
+ for(PivotTableGroupBy column : columns)
+ {
+ clone.columns.add(column.clone());
+ }
+ }
+
+ if(values != null)
+ {
+ clone.values = new ArrayList<>();
+ for(PivotTableValue value : values)
+ {
+ clone.values.add(value.clone());
+ }
+ }
+
+ return (clone);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public PivotTableDefinition withRow(PivotTableGroupBy row)
+ {
+ if(this.rows == null)
+ {
+ this.rows = new ArrayList<>();
+ }
+ this.rows.add(row);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public PivotTableDefinition withColumn(PivotTableGroupBy column)
+ {
+ if(this.columns == null)
+ {
+ this.columns = new ArrayList<>();
+ }
+ this.columns.add(column);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public PivotTableDefinition withValue(PivotTableValue value)
+ {
+ if(this.values == null)
+ {
+ this.values = new ArrayList<>();
+ }
+ this.values.add(value);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for rows
+ *******************************************************************************/
+ public List getRows()
+ {
+ return (this.rows);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for rows
+ *******************************************************************************/
+ public void setRows(List rows)
+ {
+ this.rows = rows;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for rows
+ *******************************************************************************/
+ public PivotTableDefinition withRows(List rows)
+ {
+ this.rows = rows;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for columns
+ *******************************************************************************/
+ public List getColumns()
+ {
+ return (this.columns);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for columns
+ *******************************************************************************/
+ public void setColumns(List columns)
+ {
+ this.columns = columns;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for columns
+ *******************************************************************************/
+ public PivotTableDefinition withColumns(List columns)
+ {
+ this.columns = columns;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for values
+ *******************************************************************************/
+ public List getValues()
+ {
+ return (this.values);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for values
+ *******************************************************************************/
+ public void setValues(List values)
+ {
+ this.values = values;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for values
+ *******************************************************************************/
+ public PivotTableDefinition withValues(List values)
+ {
+ this.values = values;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableFunction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableFunction.java
new file mode 100644
index 00000000..a3cd1518
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableFunction.java
@@ -0,0 +1,65 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
+
+
+/*******************************************************************************
+ ** Functions that can be applied to Values in a pivot table.
+ *******************************************************************************/
+public enum PivotTableFunction
+{
+ AVERAGE("Average"),
+ COUNT("Count Values (COUNTA)"),
+ COUNT_NUMS("Count Numbers (COUNT)"),
+ MAX("Max"),
+ MIN("Min"),
+ PRODUCT("Product"),
+ STD_DEV("StdDev"),
+ STD_DEVP("StdDevp"),
+ SUM("Sum"),
+ VAR("Var"),
+ VARP("Varp");
+
+
+ private final String label;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ PivotTableFunction(String label)
+ {
+ this.label = label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for label
+ **
+ *******************************************************************************/
+ public String getLabel()
+ {
+ return label;
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableGroupBy.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableGroupBy.java
new file mode 100644
index 00000000..11d1c81e
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableGroupBy.java
@@ -0,0 +1,143 @@
+/*
+ * 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.model.actions.reporting.pivottable;
+
+
+import java.io.Serializable;
+
+
+/*******************************************************************************
+ ** Either a row or column grouping in a pivot table. e.g., a field plus
+ ** sorting details, plus showTotals boolean.
+ *******************************************************************************/
+public class PivotTableGroupBy implements Cloneable, Serializable
+{
+ private String fieldName;
+ private PivotTableOrderBy orderBy;
+ private boolean showTotals;
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldName
+ *******************************************************************************/
+ public String getFieldName()
+ {
+ return (this.fieldName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldName
+ *******************************************************************************/
+ public void setFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldName
+ *******************************************************************************/
+ public PivotTableGroupBy withFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for orderBy
+ *******************************************************************************/
+ public PivotTableOrderBy getOrderBy()
+ {
+ return (this.orderBy);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for orderBy
+ *******************************************************************************/
+ public void setOrderBy(PivotTableOrderBy orderBy)
+ {
+ this.orderBy = orderBy;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for orderBy
+ *******************************************************************************/
+ public PivotTableGroupBy withOrderBy(PivotTableOrderBy orderBy)
+ {
+ this.orderBy = orderBy;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for showTotals
+ *******************************************************************************/
+ public boolean getShowTotals()
+ {
+ return (this.showTotals);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for showTotals
+ *******************************************************************************/
+ public void setShowTotals(boolean showTotals)
+ {
+ this.showTotals = showTotals;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for showTotals
+ *******************************************************************************/
+ public PivotTableGroupBy withShowTotals(boolean showTotals)
+ {
+ this.showTotals = showTotals;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public PivotTableGroupBy clone() throws CloneNotSupportedException
+ {
+ PivotTableGroupBy clone = (PivotTableGroupBy) super.clone();
+ return clone;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableOrderBy.java
similarity index 75%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableOrderBy.java
index 3fdd189a..d0dadc5e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableOrderBy.java
@@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2022. Kingsrook, LLC
+ * Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@@ -19,13 +19,16 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
+package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
+
+
+import java.io.Serializable;
/*******************************************************************************
- ** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
+ ** How a group-by (rows or columns) should be sorted.
*******************************************************************************/
-public class PlainExcelStyler implements ExcelStylerInterface
+public class PivotTableOrderBy implements Serializable
{
-
+ // todo - implement, but only if POI supports (or we build our own support...)
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableValue.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableValue.java
new file mode 100644
index 00000000..035b8855
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/reporting/pivottable/PivotTableValue.java
@@ -0,0 +1,110 @@
+/*
+ * 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.model.actions.reporting.pivottable;
+
+
+import java.io.Serializable;
+
+
+/*******************************************************************************
+ ** a value (e.g., field name + function) used in a pivot table
+ *******************************************************************************/
+public class PivotTableValue implements Cloneable, Serializable
+{
+ private String fieldName;
+ private PivotTableFunction function;
+
+
+
+ /*******************************************************************************
+ ** Getter for fieldName
+ *******************************************************************************/
+ public String getFieldName()
+ {
+ return (this.fieldName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fieldName
+ *******************************************************************************/
+ public void setFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fieldName
+ *******************************************************************************/
+ public PivotTableValue withFieldName(String fieldName)
+ {
+ this.fieldName = fieldName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for function
+ *******************************************************************************/
+ public PivotTableFunction getFunction()
+ {
+ return (this.function);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for function
+ *******************************************************************************/
+ public void setFunction(PivotTableFunction function)
+ {
+ this.function = function;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for function
+ *******************************************************************************/
+ public PivotTableValue withFunction(PivotTableFunction function)
+ {
+ this.function = function;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public PivotTableValue clone() throws CloneNotSupportedException
+ {
+ PivotTableValue clone = (PivotTableValue) super.clone();
+ return clone;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
index 3d07c4c2..fffd3c9e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
@@ -30,6 +30,7 @@ import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@@ -409,9 +410,20 @@ public class QQueryFilter implements Serializable, Cloneable
for(Serializable value : criterion.getValues())
{
- String valueAsString = ValueUtils.getValueAsString(value);
- Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
- newValues.add(interpretedValue);
+ if(value instanceof AbstractFilterExpression>)
+ {
+ /////////////////////////////////////////////////////////////////////////
+ // todo - do we want to try to interpret values within the expression? //
+ // e.g., greater than now minus ${input.noOfDays} //
+ /////////////////////////////////////////////////////////////////////////
+ newValues.add(value);
+ }
+ else
+ {
+ String valueAsString = ValueUtils.getValueAsString(value);
+ Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
+ newValues.add(interpretedValue);
+ }
}
criterion.setValues(newValues);
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/storage/StorageInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/storage/StorageInput.java
new file mode 100644
index 00000000..5407faeb
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/storage/StorageInput.java
@@ -0,0 +1,77 @@
+/*
+ * 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.model.actions.tables.storage;
+
+
+import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
+
+
+/*******************************************************************************
+ ** Input for Storage actions.
+ *******************************************************************************/
+public class StorageInput extends AbstractTableActionInput
+{
+ private String reference;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public StorageInput(String storageTableName)
+ {
+ super();
+ setTableName(storageTableName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for reference
+ *******************************************************************************/
+ public String getReference()
+ {
+ return (this.reference);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for reference
+ *******************************************************************************/
+ public void setReference(String reference)
+ {
+ this.reference = reference;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for reference
+ *******************************************************************************/
+ public StorageInput withReference(String reference)
+ {
+ this.reference = reference;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java
index 421164a5..a71ff27e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/WidgetType.java
@@ -50,7 +50,9 @@ public enum WidgetType
USA_MAP("usaMap"),
COMPOSITE("composite"),
DATA_BAG_VIEWER("dataBagViewer"),
- SCRIPT_VIEWER("scriptViewer");
+ SCRIPT_VIEWER("scriptViewer"),
+ REPORT_SETUP("reportSetup"),
+ PIVOT_TABLE_SETUP("pivotTableSetup");
private final String type;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java
index 8744362c..c13b87f5 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QField.java
@@ -26,6 +26,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
@@ -88,6 +89,11 @@ public @interface QField
*******************************************************************************/
ValueTooLongBehavior valueTooLongBehavior() default ValueTooLongBehavior.PASS_THROUGH;
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ DynamicDefaultValueBehavior dynamicDefaultValueBehavior() default DynamicDefaultValueBehavior.NONE;
+
//////////////////////////////////////////////////////////////////////////////////////////
// new attributes here likely need implementation in QFieldMetaData.constructFromGetter //
//////////////////////////////////////////////////////////////////////////////////////////
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java
index b6ea0975..e09673a3 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/MetaDataProducerHelper.java
@@ -150,7 +150,7 @@ public class MetaDataProducerHelper
}
catch(Exception e)
{
- LOG.warn("error executing metaDataProducer", logPair("producer", producer.getClass().getSimpleName()), e);
+ LOG.warn("error executing metaDataProducer", e, logPair("producer", producer.getClass().getSimpleName()));
}
}
else
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java
index eb63b7f4..628fa870 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QBackendMetaData.java
@@ -155,18 +155,6 @@ public class QBackendMetaData implements TopLevelMetaDataInterface
- /*******************************************************************************
- ** Fluent setter, returning generically, to help sub-class fluent flows
- *******************************************************************************/
- @SuppressWarnings("unchecked")
- public T withBackendType(String backendType)
- {
- this.backendType = backendType;
- return (T) this;
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java
index 03688222..e7793b03 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java
@@ -24,11 +24,15 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard;
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.instances.QInstanceHelpContentManager;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
@@ -61,7 +65,7 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
protected Map icons;
- protected Map helpContent;
+ protected Map> helpContent;
protected Map defaultValues = new LinkedHashMap<>();
@@ -691,10 +695,11 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
}
+
/*******************************************************************************
** Getter for helpContent
*******************************************************************************/
- public Map getHelpContent()
+ public Map> getHelpContent()
{
return (this.helpContent);
}
@@ -704,7 +709,7 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
/*******************************************************************************
** Setter for helpContent
*******************************************************************************/
- public void setHelpContent(Map helpContent)
+ public void setHelpContent(Map> helpContent)
{
this.helpContent = helpContent;
}
@@ -714,11 +719,49 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
/*******************************************************************************
** Fluent setter for helpContent
*******************************************************************************/
- public QWidgetMetaData withHelpContent(Map helpContent)
+ public QWidgetMetaData withHelpContent(Map> helpContent)
{
this.helpContent = helpContent;
return (this);
}
+
+ /*******************************************************************************
+ ** Fluent setter for adding 1 helpContent (for a slot)
+ *******************************************************************************/
+ public QWidgetMetaData withHelpContent(String slot, QHelpContent helpContent)
+ {
+ if(this.helpContent == null)
+ {
+ this.helpContent = new HashMap<>();
+ }
+
+ List listForSlot = this.helpContent.computeIfAbsent(slot, (k) -> new ArrayList<>());
+ QInstanceHelpContentManager.putHelpContentInList(helpContent, listForSlot);
+
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** remove a helpContent for a slot based on its set of roles
+ *******************************************************************************/
+ public void removeHelpContent(String slot, Set roles)
+ {
+ if(this.helpContent == null)
+ {
+ return;
+ }
+
+ List listForSlot = this.helpContent.get(slot);
+ if(listForSlot == null)
+ {
+ return;
+ }
+
+ QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, listForSlot);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java
index 1c702e39..ed8af4e3 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java
@@ -25,10 +25,12 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
@@ -235,7 +237,7 @@ public interface QWidgetMetaDataInterface extends MetaDataWithPermissionRules, T
/*******************************************************************************
**
*******************************************************************************/
- default Map getHelpContent()
+ default Map> getHelpContent()
{
return (null);
}
@@ -244,11 +246,29 @@ public interface QWidgetMetaDataInterface extends MetaDataWithPermissionRules, T
/*******************************************************************************
**
*******************************************************************************/
- default void setHelpContent(Map helpContent)
+ default void setHelpContent(Map> helpContent)
{
LOG.debug("Setting help content in a widgetMetaData type that doesn't support it (because it didn't override the getter/setter)");
}
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default QWidgetMetaDataInterface withHelpContent(String slot, QHelpContent helpContent)
+ {
+ LOG.debug("Setting help content in a widgetMetaData type that doesn't support it (because it didn't override the getter/setter)");
+ return (this);
+ }
+
+ /*******************************************************************************
+ ** remove a helpContent for a slot based on its set of roles
+ *******************************************************************************/
+ default void removeHelpContent(String slot, Set roles)
+ {
+ LOG.debug("Setting help content in a widgetMetaData type that doesn't support it (because it didn't override the getter/setter)");
+ }
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DateTimeDisplayValueBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DateTimeDisplayValueBehavior.java
index 5345a53d..05cdf643 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DateTimeDisplayValueBehavior.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/DateTimeDisplayValueBehavior.java
@@ -118,32 +118,49 @@ public class DateTimeDisplayValueBehavior implements FieldDisplayBehavior applyCreateDate(action, recordList, table, field);
case MODIFY_DATE -> applyModifyDate(action, recordList, table, field);
+ case USER_ID -> applyUserId(action, recordList, table, field);
default -> throw new IllegalStateException("Unexpected enum value: " + this);
}
}
@@ -131,6 +136,27 @@ public enum DynamicDefaultValueBehavior implements FieldBehavior recordList, QTableMetaData table, QFieldMetaData field)
+ {
+ String fieldName = field.getName();
+ String userId = ObjectUtils.tryElse(() -> QContext.getQSession().getUser().getIdReference(), null);
+ if(StringUtils.hasContent(userId))
+ {
+ for(QRecord record : CollectionUtils.nonNullList(recordList))
+ {
+ if(!StringUtils.hasContent(record.getValueString(fieldName)))
+ {
+ record.setValue(field.getName(), userId);
+ }
+ }
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java
index 8a1447be..d78d469d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java
@@ -221,11 +221,16 @@ public class QFieldMetaData implements Cloneable
setMaxLength(fieldAnnotation.maxLength());
}
- if(fieldAnnotation.valueTooLongBehavior() != ValueTooLongBehavior.PASS_THROUGH)
+ if(fieldAnnotation.valueTooLongBehavior() != ValueTooLongBehavior.values()[0].getDefault())
{
withBehavior(fieldAnnotation.valueTooLongBehavior());
}
+ if(fieldAnnotation.dynamicDefaultValueBehavior() != DynamicDefaultValueBehavior.values()[0].getDefault())
+ {
+ withBehavior(fieldAnnotation.dynamicDefaultValueBehavior());
+ }
+
if(StringUtils.hasContent(fieldAnnotation.defaultValue()))
{
ValueUtils.getValueAsFieldType(this.type, fieldAnnotation.defaultValue());
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendWidgetMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendWidgetMetaData.java
index 4d5e5725..4312949f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendWidgetMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendWidgetMetaData.java
@@ -59,9 +59,9 @@ public class QFrontendWidgetMetaData
private boolean showReloadButton = false;
private boolean showExportButton = false;
- protected Map icons;
- protected Map helpContent;
- protected Map defaultValues;
+ protected Map icons;
+ protected Map> helpContent;
+ protected Map defaultValues;
private final boolean hasPermission;
@@ -273,7 +273,7 @@ public class QFrontendWidgetMetaData
** Getter for helpContent
**
*******************************************************************************/
- public Map getHelpContent()
+ public Map> getHelpContent()
{
return helpContent;
}
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
index 1247ddfe..fa929c0d 100644
--- 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
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting;
import java.util.ArrayList;
import java.util.List;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
@@ -40,11 +41,11 @@ public class QReportView implements Cloneable
private ReportType type;
private String titleFormat;
private List titleFields;
- private List pivotFields;
+ private List summaryFields;
- private boolean includeHeaderRow = true;
- private boolean includeTotalRow = false;
- private boolean includePivotSubTotals = false;
+ private boolean includeHeaderRow = true;
+ private boolean includeTotalRow = false;
+ private boolean includeSummarySubTotals = false;
private List columns;
private List orderByFields;
@@ -52,6 +53,9 @@ public class QReportView implements Cloneable
private QCodeReference recordTransformStep;
private QCodeReference viewCustomizer;
+ private String pivotTableSourceViewName;
+ private PivotTableDefinition pivotTableDefinition;
+
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Note: This class is Cloneable - think about if new fields added here need deep-copied in the clone method! //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -297,34 +301,34 @@ public class QReportView implements Cloneable
/*******************************************************************************
- ** Getter for pivotFields
+ ** Getter for summaryFields
**
*******************************************************************************/
- public List getPivotFields()
+ public List getSummaryFields()
{
- return pivotFields;
+ return summaryFields;
}
/*******************************************************************************
- ** Setter for pivotFields
+ ** Setter for summaryFields
**
*******************************************************************************/
- public void setPivotFields(List pivotFields)
+ public void setSummaryFields(List summaryFields)
{
- this.pivotFields = pivotFields;
+ this.summaryFields = summaryFields;
}
/*******************************************************************************
- ** Fluent setter for pivotFields
+ ** Fluent setter for summaryFields
**
*******************************************************************************/
- public QReportView withPivotFields(List pivotFields)
+ public QReportView withSummaryFields(List summaryFields)
{
- this.pivotFields = pivotFields;
+ this.summaryFields = summaryFields;
return (this);
}
@@ -399,34 +403,34 @@ public class QReportView implements Cloneable
/*******************************************************************************
- ** Getter for pivotSubTotals
+ ** Getter for summarySubTotals
**
*******************************************************************************/
- public boolean getIncludePivotSubTotals()
+ public boolean getIncludeSummarySubTotals()
{
- return includePivotSubTotals;
+ return includeSummarySubTotals;
}
/*******************************************************************************
- ** Setter for pivotSubTotals
+ ** Setter for summarySubTotals
**
*******************************************************************************/
- public void setIncludePivotSubTotals(boolean includePivotSubTotals)
+ public void setIncludeSummarySubTotals(boolean includeSummarySubTotals)
{
- this.includePivotSubTotals = includePivotSubTotals;
+ this.includeSummarySubTotals = includeSummarySubTotals;
}
/*******************************************************************************
- ** Fluent setter for pivotSubTotals
+ ** Fluent setter for summarySubTotals
**
*******************************************************************************/
- public QReportView withIncludePivotSubTotals(boolean pivotSubTotals)
+ public QReportView withIncludeSummarySubTotals(boolean summarySubTotals)
{
- this.includePivotSubTotals = pivotSubTotals;
+ this.includeSummarySubTotals = summarySubTotals;
return (this);
}
@@ -602,9 +606,9 @@ public class QReportView implements Cloneable
clone.setTitleFields(new ArrayList<>(titleFields));
}
- if(pivotFields != null)
+ if(summaryFields != null)
{
- clone.setPivotFields(new ArrayList<>(pivotFields));
+ clone.setSummaryFields(new ArrayList<>(summaryFields));
}
if(columns != null)
@@ -624,4 +628,67 @@ public class QReportView implements Cloneable
throw new AssertionError();
}
}
+
+
+
+ /*******************************************************************************
+ ** Getter for pivotTableSourceViewName
+ *******************************************************************************/
+ public String getPivotTableSourceViewName()
+ {
+ return (this.pivotTableSourceViewName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for pivotTableSourceViewName
+ *******************************************************************************/
+ public void setPivotTableSourceViewName(String pivotTableSourceViewName)
+ {
+ this.pivotTableSourceViewName = pivotTableSourceViewName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for pivotTableSourceViewName
+ *******************************************************************************/
+ public QReportView withPivotTableSourceViewName(String pivotTableSourceViewName)
+ {
+ this.pivotTableSourceViewName = pivotTableSourceViewName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for pivotTableDefinition
+ *******************************************************************************/
+ public PivotTableDefinition getPivotTableDefinition()
+ {
+ return (this.pivotTableDefinition);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for pivotTableDefinition
+ *******************************************************************************/
+ public void setPivotTableDefinition(PivotTableDefinition pivotTableDefinition)
+ {
+ this.pivotTableDefinition = pivotTableDefinition;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for pivotTableDefinition
+ *******************************************************************************/
+ public QReportView withPivotTableDefinition(PivotTableDefinition pivotTableDefinition)
+ {
+ this.pivotTableDefinition = pivotTableDefinition;
+ 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
index 8492537a..22b5cb67 100644
--- 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
@@ -28,6 +28,6 @@ package com.kingsrook.qqq.backend.core.model.metadata.reporting;
public enum ReportType
{
TABLE, // e.g., raw data in tabular form.
- SUMMARY, // e.g., summaries computed within QQQ
- PIVOT // e.g., a true spreadsheet pivot. Not initially supported...
+ SUMMARY, // e.g., summaries computed within QQQ.
+ PIVOT // e.g., a true spreadsheet pivot.
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/RenderedReport.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/RenderedReport.java
new file mode 100644
index 00000000..19ef870d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/RenderedReport.java
@@ -0,0 +1,506 @@
+/*
+ * 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.model.savedreports;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
+import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
+
+
+/*******************************************************************************
+ ** Entity bean for the rendered report table
+ *******************************************************************************/
+public class RenderedReport extends QRecordEntity
+{
+ public static final String TABLE_NAME = "renderedReport";
+
+ @QField(isEditable = false)
+ private Integer id;
+
+ @QField(isEditable = false)
+ private Instant createDate;
+
+ @QField(isEditable = false)
+ private Instant modifyDate;
+
+ @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.USER_ID)
+ private String userId;
+
+ @QField(possibleValueSourceName = SavedReport.TABLE_NAME)
+ private Integer savedReportId;
+
+ @QField(possibleValueSourceName = RenderedReportStatus.NAME, label = "Status")
+ private Integer renderedReportStatusId;
+
+ @QField(maxLength = 40, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
+ private String jobUuid;
+
+ @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
+ private String resultPath;
+
+ @QField(maxLength = 10, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = ReportFormatPossibleValueEnum.NAME)
+ private String reportFormat;
+
+ @QField()
+ private Instant startTime;
+
+ @QField()
+ private Instant endTime;
+
+ @QField(displayFormat = DisplayFormat.COMMAS)
+ private Integer rowCount;
+
+ @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
+ private String errorMessage;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public RenderedReport()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public RenderedReport(QRecord qRecord) throws QException
+ {
+ populateFromQRecord(qRecord);
+ }
+
+
+ /*******************************************************************************
+ ** Getter for id
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return (this.id);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for id
+ *******************************************************************************/
+ public void setId(Integer id)
+ {
+ this.id = id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for id
+ *******************************************************************************/
+ public RenderedReport withId(Integer id)
+ {
+ this.id = id;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for createDate
+ *******************************************************************************/
+ public Instant getCreateDate()
+ {
+ return (this.createDate);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for createDate
+ *******************************************************************************/
+ public void setCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for createDate
+ *******************************************************************************/
+ public RenderedReport withCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for modifyDate
+ *******************************************************************************/
+ public Instant getModifyDate()
+ {
+ return (this.modifyDate);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for modifyDate
+ *******************************************************************************/
+ public void setModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for modifyDate
+ *******************************************************************************/
+ public RenderedReport withModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userId
+ *******************************************************************************/
+ public String getUserId()
+ {
+ return (this.userId);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userId
+ *******************************************************************************/
+ public void setUserId(String userId)
+ {
+ this.userId = userId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userId
+ *******************************************************************************/
+ public RenderedReport withUserId(String userId)
+ {
+ this.userId = userId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for savedReportId
+ *******************************************************************************/
+ public Integer getSavedReportId()
+ {
+ return (this.savedReportId);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for savedReportId
+ *******************************************************************************/
+ public void setSavedReportId(Integer savedReportId)
+ {
+ this.savedReportId = savedReportId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for savedReportId
+ *******************************************************************************/
+ public RenderedReport withSavedReportId(Integer savedReportId)
+ {
+ this.savedReportId = savedReportId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for renderedReportStatusId
+ *******************************************************************************/
+ public Integer getRenderedReportStatusId()
+ {
+ return (this.renderedReportStatusId);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for renderedReportStatusId
+ *******************************************************************************/
+ public void setRenderedReportStatusId(Integer renderedReportStatusId)
+ {
+ this.renderedReportStatusId = renderedReportStatusId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for renderedReportStatusId
+ *******************************************************************************/
+ public RenderedReport withRenderedReportStatusId(Integer renderedReportStatusId)
+ {
+ this.renderedReportStatusId = renderedReportStatusId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for jobUuid
+ *******************************************************************************/
+ public String getJobUuid()
+ {
+ return (this.jobUuid);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for jobUuid
+ *******************************************************************************/
+ public void setJobUuid(String jobUuid)
+ {
+ this.jobUuid = jobUuid;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for jobUuid
+ *******************************************************************************/
+ public RenderedReport withJobUuid(String jobUuid)
+ {
+ this.jobUuid = jobUuid;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for resultPath
+ *******************************************************************************/
+ public String getResultPath()
+ {
+ return (this.resultPath);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for resultPath
+ *******************************************************************************/
+ public void setResultPath(String resultPath)
+ {
+ this.resultPath = resultPath;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for resultPath
+ *******************************************************************************/
+ public RenderedReport withResultPath(String resultPath)
+ {
+ this.resultPath = resultPath;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for reportFormat
+ *******************************************************************************/
+ public String getReportFormat()
+ {
+ return (this.reportFormat);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for reportFormat
+ *******************************************************************************/
+ public void setReportFormat(String reportFormat)
+ {
+ this.reportFormat = reportFormat;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for reportFormat
+ *******************************************************************************/
+ public RenderedReport withReportFormat(String reportFormat)
+ {
+ this.reportFormat = reportFormat;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for startTime
+ *******************************************************************************/
+ public Instant getStartTime()
+ {
+ return (this.startTime);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for startTime
+ *******************************************************************************/
+ public void setStartTime(Instant startTime)
+ {
+ this.startTime = startTime;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for startTime
+ *******************************************************************************/
+ public RenderedReport withStartTime(Instant startTime)
+ {
+ this.startTime = startTime;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for endTime
+ *******************************************************************************/
+ public Instant getEndTime()
+ {
+ return (this.endTime);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for endTime
+ *******************************************************************************/
+ public void setEndTime(Instant endTime)
+ {
+ this.endTime = endTime;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for endTime
+ *******************************************************************************/
+ public RenderedReport withEndTime(Instant endTime)
+ {
+ this.endTime = endTime;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for rowCount
+ *******************************************************************************/
+ public Integer getRowCount()
+ {
+ return (this.rowCount);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for rowCount
+ *******************************************************************************/
+ public void setRowCount(Integer rowCount)
+ {
+ this.rowCount = rowCount;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for rowCount
+ *******************************************************************************/
+ public RenderedReport withRowCount(Integer rowCount)
+ {
+ this.rowCount = rowCount;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for errorMessage
+ *******************************************************************************/
+ public String getErrorMessage()
+ {
+ return (this.errorMessage);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for errorMessage
+ *******************************************************************************/
+ public void setErrorMessage(String errorMessage)
+ {
+ this.errorMessage = errorMessage;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for errorMessage
+ *******************************************************************************/
+ public RenderedReport withErrorMessage(String errorMessage)
+ {
+ this.errorMessage = errorMessage;
+ return (this);
+ }
+
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/RenderedReportStatus.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/RenderedReportStatus.java
new file mode 100644
index 00000000..b407fc07
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/RenderedReportStatus.java
@@ -0,0 +1,96 @@
+/*
+ * 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.model.savedreports;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public enum RenderedReportStatus implements PossibleValueEnum
+{
+ RUNNING(1, "Running"),
+ COMPLETE(2, "Complete"),
+ FAILED(3, "Failed");
+
+ public static final String NAME = "renderedReportStatus";
+
+ private final Integer id;
+ private final String label;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ RenderedReportStatus(int id, String label)
+ {
+ this.id = id;
+ this.label = label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ **
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for label
+ **
+ *******************************************************************************/
+ public String getLabel()
+ {
+ return label;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Integer getPossibleValueId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getPossibleValueLabel()
+ {
+ return label;
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumn.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumn.java
new file mode 100644
index 00000000..44518fe2
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumn.java
@@ -0,0 +1,98 @@
+/*
+ * 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.model.savedreports;
+
+
+import java.io.Serializable;
+
+
+/*******************************************************************************
+ ** single entry in ReportColumns object - as part of SavedReport
+ *******************************************************************************/
+public class ReportColumn implements Serializable
+{
+ private String name;
+ private Boolean isVisible;
+
+
+
+ /*******************************************************************************
+ ** Getter for name
+ *******************************************************************************/
+ public String getName()
+ {
+ return (this.name);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for name
+ *******************************************************************************/
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for name
+ *******************************************************************************/
+ public ReportColumn withName(String name)
+ {
+ this.name = name;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for isVisible
+ *******************************************************************************/
+ public Boolean getIsVisible()
+ {
+ return (this.isVisible);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for isVisible
+ *******************************************************************************/
+ public void setIsVisible(Boolean isVisible)
+ {
+ this.isVisible = isVisible;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for isVisible
+ *******************************************************************************/
+ public ReportColumn withIsVisible(Boolean isVisible)
+ {
+ this.isVisible = isVisible;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumns.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumns.java
new file mode 100644
index 00000000..a18e1dd9
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/ReportColumns.java
@@ -0,0 +1,117 @@
+/*
+ * 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.model.savedreports;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ ** type of object expected to be in the SavedReport columnsJSON field
+ *******************************************************************************/
+public class ReportColumns implements Serializable
+{
+ private List columns;
+
+
+
+ /*******************************************************************************
+ ** Getter for columns
+ *******************************************************************************/
+ public List getColumns()
+ {
+ return (this.columns);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public List extractVisibleColumns()
+ {
+ return CollectionUtils.nonNullList(getColumns()).stream()
+ //////////////////////////////////////////////////////
+ // if isVisible is missing, we assume it to be true //
+ //////////////////////////////////////////////////////
+ .filter(rc -> rc.getIsVisible() == null || rc.getIsVisible())
+ .filter(rc -> StringUtils.hasContent(rc.getName()))
+ .filter(rc -> !rc.getName().startsWith("__check"))
+ .toList();
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for columns
+ *******************************************************************************/
+ public void setColumns(List columns)
+ {
+ this.columns = columns;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for columns
+ *******************************************************************************/
+ public ReportColumns withColumns(List columns)
+ {
+ this.columns = columns;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter to add 1 column
+ *******************************************************************************/
+ public ReportColumns withColumn(ReportColumn column)
+ {
+ if(this.columns == null)
+ {
+ this.columns = new ArrayList<>();
+ }
+ this.columns.add(column);
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter to add 1 column w/ just a name
+ *******************************************************************************/
+ public ReportColumns withColumn(String name)
+ {
+ if(this.columns == null)
+ {
+ this.columns = new ArrayList<>();
+ }
+ this.columns.add(new ReportColumn().withName(name));
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReport.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReport.java
new file mode 100644
index 00000000..190efbd1
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReport.java
@@ -0,0 +1,386 @@
+/*
+ * 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.model.savedreports;
+
+
+import java.time.Instant;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.data.QField;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
+
+
+/*******************************************************************************
+ ** Entity bean for the saved report table
+ *******************************************************************************/
+public class SavedReport extends QRecordEntity
+{
+ public static final String TABLE_NAME = "savedReport";
+
+ @QField(isEditable = false)
+ private Integer id;
+
+ @QField(isEditable = false)
+ private Instant createDate;
+
+ @QField(isEditable = false)
+ private Instant modifyDate;
+
+ @QField(isRequired = true, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS, label = "Report Name")
+ private String label;
+
+ @QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME, maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, label = "Table", isRequired = true)
+ private String tableName;
+
+ @QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.USER_ID)
+ private String userId;
+
+ @QField(label = "Query Filter")
+ private String queryFilterJson;
+
+ @QField(label = "Columns")
+ private String columnsJson;
+
+ @QField(label = "Input Fields")
+ private String inputFieldsJson;
+
+ @QField(label = "Pivot Table")
+ private String pivotTableJson;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public SavedReport()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public SavedReport(QRecord qRecord) throws QException
+ {
+ populateFromQRecord(qRecord);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for id
+ **
+ *******************************************************************************/
+ public Integer getId()
+ {
+ return id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for id
+ **
+ *******************************************************************************/
+ public void setId(Integer id)
+ {
+ this.id = id;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for createDate
+ **
+ *******************************************************************************/
+ public Instant getCreateDate()
+ {
+ return createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for createDate
+ **
+ *******************************************************************************/
+ public void setCreateDate(Instant createDate)
+ {
+ this.createDate = createDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for modifyDate
+ **
+ *******************************************************************************/
+ public Instant getModifyDate()
+ {
+ return modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for modifyDate
+ **
+ *******************************************************************************/
+ public void setModifyDate(Instant modifyDate)
+ {
+ this.modifyDate = modifyDate;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for label
+ **
+ *******************************************************************************/
+ public String getLabel()
+ {
+ return label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for label
+ **
+ *******************************************************************************/
+ public void setLabel(String label)
+ {
+ this.label = label;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for label
+ **
+ *******************************************************************************/
+ public SavedReport withLabel(String label)
+ {
+ this.label = label;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for tableName
+ **
+ *******************************************************************************/
+ public String getTableName()
+ {
+ return tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for tableName
+ **
+ *******************************************************************************/
+ public void setTableName(String tableName)
+ {
+ this.tableName = tableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tableName
+ **
+ *******************************************************************************/
+ public SavedReport withTableName(String tableName)
+ {
+ this.tableName = tableName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userId
+ **
+ *******************************************************************************/
+ public String getUserId()
+ {
+ return userId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userId
+ **
+ *******************************************************************************/
+ public void setUserId(String userId)
+ {
+ this.userId = userId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userId
+ **
+ *******************************************************************************/
+ public SavedReport withUserId(String userId)
+ {
+ this.userId = userId;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for queryFilterJson
+ *******************************************************************************/
+ public String getQueryFilterJson()
+ {
+ return (this.queryFilterJson);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for queryFilterJson
+ *******************************************************************************/
+ public void setQueryFilterJson(String queryFilterJson)
+ {
+ this.queryFilterJson = queryFilterJson;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for queryFilterJson
+ *******************************************************************************/
+ public SavedReport withQueryFilterJson(String queryFilterJson)
+ {
+ this.queryFilterJson = queryFilterJson;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for columnsJson
+ *******************************************************************************/
+ public String getColumnsJson()
+ {
+ return (this.columnsJson);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for columnsJson
+ *******************************************************************************/
+ public void setColumnsJson(String columnsJson)
+ {
+ this.columnsJson = columnsJson;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for columnsJson
+ *******************************************************************************/
+ public SavedReport withColumnsJson(String columnsJson)
+ {
+ this.columnsJson = columnsJson;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for inputFieldsJson
+ *******************************************************************************/
+ public String getInputFieldsJson()
+ {
+ return (this.inputFieldsJson);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for inputFieldsJson
+ *******************************************************************************/
+ public void setInputFieldsJson(String inputFieldsJson)
+ {
+ this.inputFieldsJson = inputFieldsJson;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for inputFieldsJson
+ *******************************************************************************/
+ public SavedReport withInputFieldsJson(String inputFieldsJson)
+ {
+ this.inputFieldsJson = inputFieldsJson;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for pivotTableJson
+ *******************************************************************************/
+ public String getPivotTableJson()
+ {
+ return (this.pivotTableJson);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for pivotTableJson
+ *******************************************************************************/
+ public void setPivotTableJson(String pivotTableJson)
+ {
+ this.pivotTableJson = pivotTableJson;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for pivotTableJson
+ *******************************************************************************/
+ public SavedReport withPivotTableJson(String pivotTableJson)
+ {
+ this.pivotTableJson = pivotTableJson;
+ return (this);
+ }
+
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatter.java
new file mode 100644
index 00000000..cb1af39c
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportJsonFieldDisplayValueFormatter.java
@@ -0,0 +1,150 @@
+/*
+ * 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.model.savedreports;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior;
+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.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class SavedReportJsonFieldDisplayValueFormatter implements FieldDisplayBehavior
+{
+ private static SavedReportJsonFieldDisplayValueFormatter savedReportJsonFieldDisplayValueFormatter = null;
+
+
+
+ /*******************************************************************************
+ ** Singleton constructor
+ *******************************************************************************/
+ private SavedReportJsonFieldDisplayValueFormatter()
+ {
+
+ }
+
+
+
+ /*******************************************************************************
+ ** Singleton accessor
+ *******************************************************************************/
+ public static SavedReportJsonFieldDisplayValueFormatter getInstance()
+ {
+ if(savedReportJsonFieldDisplayValueFormatter == null)
+ {
+ savedReportJsonFieldDisplayValueFormatter = new SavedReportJsonFieldDisplayValueFormatter();
+ }
+ return (savedReportJsonFieldDisplayValueFormatter);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public SavedReportJsonFieldDisplayValueFormatter getDefault()
+ {
+ return getInstance();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void apply(ValueBehaviorApplier.Action action, List recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
+ {
+ for(QRecord record : CollectionUtils.nonNullList(recordList))
+ {
+ if(field.getName().equals("queryFilterJson"))
+ {
+ String queryFilterJson = record.getValueString("queryFilterJson");
+ if(StringUtils.hasContent(queryFilterJson))
+ {
+ try
+ {
+ QQueryFilter qQueryFilter = SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson);
+ int criteriaCount = CollectionUtils.nonNullList(qQueryFilter.getCriteria()).size();
+ record.setDisplayValue("queryFilterJson", criteriaCount + " Filter" + StringUtils.plural(criteriaCount));
+ }
+ catch(Exception e)
+ {
+ record.setDisplayValue("queryFilterJson", "Invalid Filter...");
+ }
+ }
+ }
+
+ if(field.getName().equals("columnsJson"))
+ {
+ String columnsJson = record.getValueString("columnsJson");
+ if(StringUtils.hasContent(columnsJson))
+ {
+ try
+ {
+ ReportColumns reportColumns = SavedReportToReportMetaDataAdapter.getReportColumns(columnsJson);
+ int columnCount = reportColumns.extractVisibleColumns().size();
+
+ record.setDisplayValue("columnsJson", columnCount + " Column" + StringUtils.plural(columnCount));
+ }
+ catch(Exception e)
+ {
+ record.setDisplayValue("columnsJson", "Invalid Columns...");
+ }
+ }
+ }
+
+ if(field.getName().equals("pivotTableJson"))
+ {
+ String pivotTableJson = record.getValueString("pivotTableJson");
+ if(StringUtils.hasContent(pivotTableJson))
+ {
+ try
+ {
+ PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson);
+ int rowCount = CollectionUtils.nonNullList(pivotTableDefinition.getRows()).size();
+ int columnCount = CollectionUtils.nonNullList(pivotTableDefinition.getColumns()).size();
+ int valueCount = CollectionUtils.nonNullList(pivotTableDefinition.getValues()).size();
+ record.setDisplayValue("pivotTableJson", rowCount + " Row" + StringUtils.plural(rowCount) + ", " + columnCount + " Column" + StringUtils.plural(columnCount) + ", and " + valueCount + " Value" + StringUtils.plural(valueCount));
+ }
+ catch(Exception e)
+ {
+ record.setDisplayValue("pivotTableJson", "Invalid Pivot Table...");
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizer.java
new file mode 100644
index 00000000..73679dcf
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportTableCustomizer.java
@@ -0,0 +1,282 @@
+/*
+ * 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.model.savedreports;
+
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
+import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
+import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.SavedReportToReportMetaDataAdapter;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class SavedReportTableCustomizer implements TableCustomizerInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public List preInsert(InsertInput insertInput, List records, boolean isPreview) throws QException
+ {
+ return (preInsertOrUpdate(records));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public List preUpdate(UpdateInput updateInput, List records, boolean isPreview, Optional> oldRecordList) throws QException
+ {
+ return (preInsertOrUpdate(records));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private List preInsertOrUpdate(List records)
+ {
+ for(QRecord record : CollectionUtils.nonNullList(records))
+ {
+ preValidateRecord(record);
+ }
+ return (records);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ void preValidateRecord(QRecord record)
+ {
+ try
+ {
+ String tableName = record.getValueString("tableName");
+ String queryFilterJson = record.getValueString("queryFilterJson");
+ String columnsJson = record.getValueString("columnsJson");
+ String pivotTableJson = record.getValueString("pivotTableJson");
+
+ Set usedColumns = new HashSet<>();
+
+ QTableMetaData table = QContext.getQInstance().getTable(tableName);
+ if(table == null)
+ {
+ record.addError(new BadInputStatusMessage("Unrecognized table name: " + tableName));
+ }
+
+ if(StringUtils.hasContent(queryFilterJson))
+ {
+ try
+ {
+ ////////////////////////////////////////////////////////////////
+ // nothing to validate on filter, other than, we can parse it //
+ ////////////////////////////////////////////////////////////////
+ SavedReportToReportMetaDataAdapter.getQQueryFilter(queryFilterJson);
+ }
+ catch(IOException e)
+ {
+ record.addError(new BadInputStatusMessage("Unable to parse queryFilterJson: " + e.getMessage()));
+ }
+ }
+
+ boolean hadColumnParseError = false;
+ if(StringUtils.hasContent(columnsJson))
+ {
+ try
+ {
+ /////////////////////////////////////////////////////////////////////////
+ // make sure we can parse columns, and that we have at least 1 visible //
+ /////////////////////////////////////////////////////////////////////////
+ ReportColumns reportColumns = SavedReportToReportMetaDataAdapter.getReportColumns(columnsJson);
+ for(ReportColumn column : reportColumns.extractVisibleColumns())
+ {
+ usedColumns.add(column.getName());
+ }
+ }
+ catch(IOException e)
+ {
+ record.addError(new BadInputStatusMessage("Unable to parse columnsJson: " + e.getMessage()));
+ hadColumnParseError = true;
+ }
+ }
+
+ if(usedColumns.isEmpty() && !hadColumnParseError)
+ {
+ record.addError(new BadInputStatusMessage("A Report must contain at least 1 column"));
+ }
+
+ if(StringUtils.hasContent(pivotTableJson))
+ {
+ try
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // make sure we can parse pivot table, and we have ... at least 1 ... row? maybe that's all that's needed //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ PivotTableDefinition pivotTableDefinition = SavedReportToReportMetaDataAdapter.getPivotTableDefinition(pivotTableJson);
+ boolean anyRows = false;
+ boolean missingAnyFieldNamesInRows = false;
+ boolean missingAnyFieldNamesInColumns = false;
+ boolean missingAnyFieldNamesInValues = false;
+ boolean missingAnyFunctionsInValues = false;
+
+ //////////////////
+ // look at rows //
+ //////////////////
+ for(PivotTableGroupBy row : CollectionUtils.nonNullList(pivotTableDefinition.getRows()))
+ {
+ anyRows = true;
+ if(StringUtils.hasContent(row.getFieldName()))
+ {
+ if(!usedColumns.contains(row.getFieldName()) && !hadColumnParseError)
+ {
+ record.addError(new BadInputStatusMessage("A pivot table row is using field (" + getFieldLabelElseName(table, row.getFieldName()) + ") which is not an active column on this report."));
+ }
+ }
+ else
+ {
+ missingAnyFieldNamesInRows = true;
+ }
+ }
+
+ if(!anyRows)
+ {
+ record.addError(new BadInputStatusMessage("A Pivot Table must contain at least 1 row"));
+ }
+
+ /////////////////////
+ // look at columns //
+ /////////////////////
+ for(PivotTableGroupBy column : CollectionUtils.nonNullList(pivotTableDefinition.getColumns()))
+ {
+ if(StringUtils.hasContent(column.getFieldName()))
+ {
+ if(!usedColumns.contains(column.getFieldName()) && !hadColumnParseError)
+ {
+ record.addError(new BadInputStatusMessage("A pivot table column is using field (" + getFieldLabelElseName(table, column.getFieldName()) + ") which is not an active column on this report."));
+ }
+ }
+ else
+ {
+ missingAnyFieldNamesInColumns = true;
+ }
+ }
+
+ ////////////////////
+ // look at values //
+ ////////////////////
+ for(PivotTableValue value : CollectionUtils.nonNullList(pivotTableDefinition.getValues()))
+ {
+ if(StringUtils.hasContent(value.getFieldName()))
+ {
+ if(!usedColumns.contains(value.getFieldName()) && !hadColumnParseError)
+ {
+ record.addError(new BadInputStatusMessage("A pivot table value is using field (" + getFieldLabelElseName(table, value.getFieldName()) + ") which is not an active column on this report."));
+ }
+ }
+ else
+ {
+ missingAnyFieldNamesInValues = true;
+ }
+
+ if(value.getFunction() == null)
+ {
+ missingAnyFunctionsInValues = true;
+ }
+ }
+
+ ////////////////////////////////////////////////
+ // errors based on missing things found above //
+ ////////////////////////////////////////////////
+ if(missingAnyFieldNamesInRows)
+ {
+ record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table row."));
+ }
+
+ if(missingAnyFieldNamesInColumns)
+ {
+ record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table column."));
+ }
+
+ if(missingAnyFieldNamesInValues)
+ {
+ record.addError(new BadInputStatusMessage("Missing field name for at least one pivot table value."));
+ }
+
+ if(missingAnyFunctionsInValues)
+ {
+ record.addError(new BadInputStatusMessage("Missing function for at least one pivot table value."));
+ }
+ }
+ catch(IOException e)
+ {
+ record.addError(new BadInputStatusMessage("Unable to parse pivotTableJson: " + e.getMessage()));
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error validating a savedReport");
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String getFieldLabelElseName(QTableMetaData table, String fieldName)
+ {
+ try
+ {
+ GenerateReportAction.FieldAndJoinTable fieldAndJoinTable = GenerateReportAction.getFieldAndJoinTable(table, fieldName);
+ return (fieldAndJoinTable.getLabel(table));
+ }
+ catch(Exception e)
+ {
+ return (fieldName);
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java
new file mode 100644
index 00000000..b82e648f
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedreports/SavedReportsMetaDataProvider.java
@@ -0,0 +1,210 @@
+/*
+ * 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.model.savedreports;
+
+
+import java.util.List;
+import java.util.function.Consumer;
+import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
+import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.DefaultWidgetRenderer;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
+import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
+import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
+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.QIcon;
+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.tables.Capability;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
+import com.kingsrook.qqq.backend.core.processes.implementations.savedreports.RenderSavedReportMetaDataProducer;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class SavedReportsMetaDataProvider
+{
+ public static final String REPORT_STORAGE_TABLE_NAME = "reportStorage";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void defineAll(QInstance instance, String recordTablesBackendName, String reportStorageBackendName, Consumer backendDetailEnricher) throws QException
+ {
+ instance.addTable(defineSavedReportTable(recordTablesBackendName, backendDetailEnricher));
+ instance.addTable(defineRenderedReportTable(recordTablesBackendName, backendDetailEnricher));
+ instance.addPossibleValueSource(QPossibleValueSource.newForTable(SavedReport.TABLE_NAME));
+ instance.addPossibleValueSource(QPossibleValueSource.newForEnum(ReportFormatPossibleValueEnum.NAME, ReportFormatPossibleValueEnum.values()));
+ instance.addPossibleValueSource(QPossibleValueSource.newForEnum(RenderedReportStatus.NAME, RenderedReportStatus.values()));
+
+ instance.addTable(defineReportStorageTable(reportStorageBackendName, backendDetailEnricher));
+
+ QProcessMetaData renderSavedReportProcess = new RenderSavedReportMetaDataProducer().produce(instance);
+ instance.addProcess(renderSavedReportProcess);
+ renderSavedReportProcess.getInputFields().stream()
+ .filter(f -> RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_TABLE_NAME.equals(f.getName()))
+ .findFirst()
+ .ifPresent(f -> f.setDefaultValue(REPORT_STORAGE_TABLE_NAME));
+
+ instance.addWidget(defineReportSetupWidget());
+ instance.addWidget(definePivotTableSetupWidget());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineReportStorageTable(String backendName, Consumer backendDetailEnricher)
+ {
+ QTableMetaData table = new QTableMetaData()
+ .withName(REPORT_STORAGE_TABLE_NAME)
+ .withBackendName(backendName)
+ .withPrimaryKeyField("reference")
+ .withField(new QFieldMetaData("reference", QFieldType.STRING))
+ .withField(new QFieldMetaData("contents", QFieldType.BLOB));
+
+ if(backendDetailEnricher != null)
+ {
+ backendDetailEnricher.accept(table);
+ }
+
+ return (table);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QWidgetMetaDataInterface defineReportSetupWidget()
+ {
+ return new QWidgetMetaData()
+ .withName("reportSetupWidget")
+ .withLabel("Filters and Columns")
+ .withIsCard(true)
+ .withType(WidgetType.REPORT_SETUP.getType())
+ .withCodeReference(new QCodeReference(DefaultWidgetRenderer.class));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QWidgetMetaDataInterface definePivotTableSetupWidget()
+ {
+ return new QWidgetMetaData()
+ .withName("pivotTableSetupWidget")
+ .withLabel("Pivot Table")
+ .withIsCard(true)
+ .withType(WidgetType.PIVOT_TABLE_SETUP.getType())
+ .withCodeReference(new QCodeReference(DefaultWidgetRenderer.class));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QTableMetaData defineSavedReportTable(String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ QTableMetaData table = new QTableMetaData()
+ .withName(SavedReport.TABLE_NAME)
+ .withLabel("Report")
+ .withIcon(new QIcon().withName("article"))
+ .withRecordLabelFormat("%s")
+ .withRecordLabelFields("label")
+ .withBackendName(backendName)
+ .withPrimaryKeyField("id")
+ .withFieldsFromEntity(SavedReport.class)
+ .withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.FIELD))
+ .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label", "tableName")))
+ .withSection(new QFieldSection("filtersAndColumns", new QIcon().withName("table_chart"), Tier.T2).withLabel("Filters and Columns").withWidgetName("reportSetupWidget"))
+ .withSection(new QFieldSection("pivotTable", new QIcon().withName("pivot_table_chart"), Tier.T2).withLabel("Pivot Table").withWidgetName("pivotTableSetupWidget"))
+ .withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("queryFilterJson", "columnsJson", "pivotTableJson")).withIsHidden(true))
+ .withSection(new QFieldSection("hidden", new QIcon().withName("text_snippet"), Tier.T2, List.of("inputFieldsJson", "userId")).withIsHidden(true))
+ .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
+
+ table.getField("queryFilterJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance());
+ table.getField("columnsJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance());
+ table.getField("pivotTableJson").withBehavior(SavedReportJsonFieldDisplayValueFormatter.getInstance());
+
+ table.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(SavedReportTableCustomizer.class));
+ table.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(SavedReportTableCustomizer.class));
+
+ if(backendDetailEnricher != null)
+ {
+ backendDetailEnricher.accept(table);
+ }
+
+ return (table);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private QTableMetaData defineRenderedReportTable(String backendName, Consumer backendDetailEnricher) throws QException
+ {
+ QTableMetaData table = new QTableMetaData()
+ .withName(RenderedReport.TABLE_NAME)
+ .withIcon(new QIcon().withName("print"))
+ .withRecordLabelFormat("%s - %s")
+ .withRecordLabelFields("savedReportId", "startTime")
+ .withBackendName(backendName)
+ .withPrimaryKeyField("id")
+ .withFieldsFromEntity(RenderedReport.class)
+ .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "savedReportId", "renderedReportStatusId")))
+ .withSection(new QFieldSection("input", new QIcon().withName("input"), Tier.T2, List.of("userId", "reportFormat")))
+ .withSection(new QFieldSection("output", new QIcon().withName("output"), Tier.T2, List.of("jobUuid", "resultPath", "rowCount", "errorMessage", "startTime", "endTime")))
+ .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))
+ .withoutCapabilities(Capability.allWriteCapabilities());
+
+ table.getField("renderedReportStatusId").setAdornments(List.of(new FieldAdornment(AdornmentType.CHIP)
+ .withValues(AdornmentType.ChipValues.iconAndColorValues(RenderedReportStatus.RUNNING.getId(), "pending", AdornmentType.ChipValues.COLOR_SECONDARY))
+ .withValues(AdornmentType.ChipValues.iconAndColorValues(RenderedReportStatus.COMPLETE.getId(), "check", AdornmentType.ChipValues.COLOR_SUCCESS))
+ .withValues(AdornmentType.ChipValues.iconAndColorValues(RenderedReportStatus.FAILED.getId(), "error", AdornmentType.ChipValues.COLOR_ERROR))));
+
+ if(backendDetailEnricher != null)
+ {
+ backendDetailEnricher.accept(table);
+ }
+
+ return (table);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedViewsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedViewsMetaDataProvider.java
index 2581e67d..ee6b16da 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedViewsMetaDataProvider.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/savedviews/SavedViewsMetaDataProvider.java
@@ -68,7 +68,8 @@ public class SavedViewsMetaDataProvider
{
QTableMetaData table = new QTableMetaData()
.withName(SavedView.TABLE_NAME)
- .withLabel("Saved View")
+ .withLabel("View")
+ .withIcon(new QIcon().withName("table_view"))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withBackendName(backendName)
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java
index ac2e313a..0ff5c246 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java
@@ -28,6 +28,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -52,9 +53,16 @@ public class QSession implements Serializable
private Map> securityKeyValues;
private Map backendVariants;
- // implementation-specific custom values
+ ///////////////////////////////////////////
+ // implementation-specific custom values //
+ ///////////////////////////////////////////
private Map values;
+ /////////////////////////////////////////////
+ // values meant to be passed to a frontend //
+ /////////////////////////////////////////////
+ private Map valuesForFrontend;
+
public static final String VALUE_KEY_USER_TIMEZONE = "UserTimezone";
public static final String VALUE_KEY_USER_TIMEZONE_OFFSET_MINUTES = "UserTimezoneOffsetMinutes";
@@ -500,4 +508,49 @@ public class QSession implements Serializable
return (this);
}
+
+ /*******************************************************************************
+ ** Getter for valuesForFrontend
+ *******************************************************************************/
+ public Map getValuesForFrontend()
+ {
+ return (this.valuesForFrontend);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for valuesForFrontend
+ *******************************************************************************/
+ public void setValuesForFrontend(Map valuesForFrontend)
+ {
+ this.valuesForFrontend = valuesForFrontend;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for valuesForFrontend
+ *******************************************************************************/
+ public QSession withValuesForFrontend(Map valuesForFrontend)
+ {
+ this.valuesForFrontend = valuesForFrontend;
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ ** Fluent setter for a single valuesForFrontend
+ *******************************************************************************/
+ public QSession withValueForFrontend(String key, Serializable value)
+ {
+ if(this.valuesForFrontend == null)
+ {
+ this.valuesForFrontend = new LinkedHashMap<>();
+ }
+ this.valuesForFrontend.put(key, value);
+ return (this);
+ }
+
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java
index a4e8525e..d8069f0d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleDispatcher.java
@@ -33,14 +33,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
** This class is responsible for loading a backend module, by its name, and
** returning an instance.
**
- ** TODO - make this mapping runtime-bound, not pre-compiled in.
- **
*******************************************************************************/
public class QBackendModuleDispatcher
{
private static final QLogger LOG = QLogger.getLogger(QBackendModuleDispatcher.class);
- private static Map backendTypeToModuleClassNameMap;
+ private static Map backendTypeToModuleClassNameMap = new HashMap<>();
@@ -49,51 +47,6 @@ public class QBackendModuleDispatcher
*******************************************************************************/
public QBackendModuleDispatcher()
{
- initBackendTypeToModuleClassNameMap();
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private static void initBackendTypeToModuleClassNameMap()
- {
- if(backendTypeToModuleClassNameMap != null)
- {
- return;
- }
-
- Map newMap = new HashMap<>();
-
- String[] moduleClassNames = new String[]
- {
- // todo - let modules somehow "export" their types here?
- // e.g., backend-core shouldn't need to "know" about the modules.
- "com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule",
- "com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule",
- "com.kingsrook.qqq.backend.core.modules.backend.implementations.enumeration.EnumerationBackendModule",
- "com.kingsrook.qqq.backend.module.rdbms.RDBMSBackendModule",
- "com.kingsrook.qqq.backend.module.filesystem.local.FilesystemBackendModule",
- "com.kingsrook.qqq.backend.module.filesystem.s3.S3BackendModule",
- "com.kingsrook.qqq.backend.module.api.APIBackendModule"
- };
-
- for(String moduleClassName : moduleClassNames)
- {
- try
- {
- Class> moduleClass = Class.forName(moduleClassName);
- QBackendModuleInterface module = (QBackendModuleInterface) moduleClass.getConstructor().newInstance();
- newMap.put(module.getBackendType(), moduleClassName);
- }
- catch(Exception e)
- {
- LOG.debug("Backend module [{}] could not be loaded: {}", moduleClassName, e.getMessage());
- }
- }
-
- backendTypeToModuleClassNameMap = newMap;
}
@@ -103,7 +56,6 @@ public class QBackendModuleDispatcher
*******************************************************************************/
public static void registerBackendModule(QBackendModuleInterface moduleInstance)
{
- initBackendTypeToModuleClassNameMap();
String backendType = moduleInstance.getBackendType();
if(backendTypeToModuleClassNameMap.containsKey(backendType))
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
index 64ce0c3c..87eef4c5 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/QBackendModuleInterface.java
@@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.QStorageInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@@ -39,7 +40,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails
/*******************************************************************************
** Interface that a QBackendModule must implement.
**
- ** Note, some methods all have a default version, which throws a 'not implemented'
+ ** Note, all methods have a default version, which throws a 'not implemented'
** exception.
**
*******************************************************************************/
@@ -129,6 +130,16 @@ public interface QBackendModuleInterface
return null;
}
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default QStorageInterface getStorageInterface()
+ {
+ throwNotImplemented("StorageInterface");
+ return null;
+ }
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java
index 7beb1ec2..53c1c594 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/enumeration/EnumerationBackendModule.java
@@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.enumerati
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableBackendDetails;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@@ -37,6 +38,10 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class EnumerationBackendModule implements QBackendModuleInterface
{
+ static
+ {
+ QBackendModuleDispatcher.registerBackendModule(new EnumerationBackendModule());
+ }
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java
index 4d6a93cb..3203280e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModule.java
@@ -26,8 +26,10 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
+import com.kingsrook.qqq.backend.core.actions.interfaces.QStorageInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@@ -42,6 +44,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class MemoryBackendModule implements QBackendModuleInterface
{
+ static
+ {
+ QBackendModuleDispatcher.registerBackendModule(new MemoryBackendModule());
+ }
+
+
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
*******************************************************************************/
@@ -117,4 +125,14 @@ public class MemoryBackendModule implements QBackendModuleInterface
return (new MemoryDeleteAction());
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QStorageInterface getStorageInterface()
+ {
+ return (new MemoryStorageAction());
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryStorageAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryStorageAction.java
new file mode 100644
index 00000000..313145a7
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryStorageAction.java
@@ -0,0 +1,149 @@
+/*
+ * 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.modules.backend.implementations.memory;
+
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.actions.interfaces.QStorageInterface;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
+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.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ ** implementation of bulk-storage interface, for the memory backend module.
+ **
+ ** Requires table to have (at least?) 2 fields - a STRING primary key and a
+ ** BLOB to store bytes.
+ *******************************************************************************/
+public class MemoryStorageAction implements QStorageInterface
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public OutputStream createOutputStream(StorageInput storageInput)
+ {
+ return new MemoryStorageOutputStream(storageInput.getTableName(), storageInput.getReference());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public InputStream getInputStream(StorageInput storageInput) throws QException
+ {
+ QRecord record = new GetAction().executeForRecord(new GetInput(storageInput.getTableName()).withPrimaryKey(storageInput.getReference()));
+ if(record == null)
+ {
+ throw (new QNotFoundException("Could not find input stream for [" + storageInput.getTableName() + "][" + storageInput.getReference() + "]"));
+ }
+
+ QFieldMetaData blobField = getBlobField(storageInput.getTableName());
+ return (new ByteArrayInputStream(record.getValueByteArray(blobField.getName())));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static QFieldMetaData getBlobField(String tableName) throws QException
+ {
+ Optional firstBlobField = QContext.getQInstance().getTable(tableName).getFields().values().stream().filter(f -> QFieldType.BLOB.equals(f.getType())).findFirst();
+ if(firstBlobField.isEmpty())
+ {
+ throw (new QException("Could not find a blob field in table [" + tableName + "]"));
+ }
+ return firstBlobField.get();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static class MemoryStorageOutputStream extends ByteArrayOutputStream
+ {
+ private final String tableName;
+ private final String reference;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public MemoryStorageOutputStream(String tableName, String reference)
+ {
+ this.tableName = tableName;
+ this.reference = reference;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void close() throws IOException
+ {
+ super.close();
+
+ try
+ {
+ QFieldMetaData blobField = getBlobField(tableName);
+ InsertOutput insertOutput = new InsertAction().execute(new InsertInput(tableName).withRecord(new QRecord()
+ .withValue(QContext.getQInstance().getTable(tableName).getPrimaryKeyField(), reference)
+ .withValue(blobField.getName(), toByteArray())));
+
+ if(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getErrors()))
+ {
+ throw(new IOException("Error storing stream into memory storage: " + StringUtils.joinWithCommasAndAnd(insertOutput.getRecords().get(0).getErrors().stream().map(e -> e.getMessage()).toList())));
+ }
+ }
+ catch(Exception e)
+ {
+ throw new IOException("Wrapped QException", e);
+ }
+ }
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockBackendModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockBackendModule.java
index 9ee92353..e530e759 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockBackendModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/mock/MockBackendModule.java
@@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
+import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@@ -40,6 +41,11 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class MockBackendModule implements QBackendModuleInterface
{
+ static
+ {
+ QBackendModuleDispatcher.registerBackendModule(new MockBackendModule());
+ }
+
/*******************************************************************************
** Method where a backend module must be able to provide its type (name).
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java
index 3aad6920..b5128b0c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/mock/MockBackendStep.java
@@ -56,7 +56,7 @@ public class MockBackendStep implements BackendStep
runBackendStepInput.getRecords().forEach(r ->
{
- LOG.info("We are mocking {}: {}", r.getValueString("firstName"), r.getValue(FIELD_MOCK_VALUE));
+ LOG.info("We are mocking " + r.getValueString("firstName") + ": " + r.getValue(FIELD_MOCK_VALUE));
r.setValue(FIELD_MOCK_VALUE, "Ha ha!");
r.setValue("greetingMessage", runBackendStepInput.getValueString(FIELD_GREETING_PREFIX) + " " + r.getValueString("firstName") + " " + runBackendStepInput.getValueString(FIELD_GREETING_SUFFIX));
});
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java
index 8d71629e..fdfa381c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/reports/ExecuteReportStep.java
@@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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.ReportDestination;
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.metadata.reporting.QReportMetaData;
@@ -66,8 +67,9 @@ public class ExecuteReportStep implements BackendStep
{
ReportInput reportInput = new ReportInput();
reportInput.setReportName(reportName);
- reportInput.setReportFormat(ReportFormat.XLSX); // todo - variable
- reportInput.setReportOutputStream(reportOutputStream);
+ reportInput.setReportDestination(new ReportDestination()
+ .withReportFormat(ReportFormat.XLSX) // todo - variable
+ .withReportOutputStream(reportOutputStream));
Map values = runBackendStepInput.getValues();
reportInput.setInputValues(values);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java
new file mode 100644
index 00000000..9cf6ba45
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java
@@ -0,0 +1,180 @@
+/*
+ * 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.processes.implementations.savedreports;
+
+
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import java.util.UUID;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+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.ReportDestination;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
+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.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
+import com.kingsrook.qqq.backend.core.model.savedreports.RenderedReport;
+import com.kingsrook.qqq.backend.core.model.savedreports.RenderedReportStatus;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
+import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** Process step to actually execute rendering a saved report.
+ *******************************************************************************/
+public class RenderSavedReportExecuteStep implements BackendStep
+{
+ private static final QLogger LOG = QLogger.getLogger(RenderSavedReportExecuteStep.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ QRecord renderedReportRecord = null;
+
+ try
+ {
+ ////////////////////////////////
+ // read inputs, set up params //
+ ////////////////////////////////
+ String storageTableName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_TABLE_NAME);
+ ReportFormat reportFormat = ReportFormat.fromString(runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_REPORT_FORMAT));
+ SavedReport savedReport = new SavedReport(runBackendStepInput.getRecords().get(0));
+ String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, savedReport);
+ String storageReference = LocalDate.now() + "/" + LocalTime.now().toString().replaceAll(":", "").replaceFirst("\\..*", "") + "/" + UUID.randomUUID() + "/" + downloadFileBaseName + "." + reportFormat.getExtension();
+ OutputStream outputStream = new StorageAction().createOutputStream(new StorageInput(storageTableName).withReference(storageReference));
+
+ LOG.info("Starting to render a report", logPair("savedReportId", savedReport.getId()), logPair("tableName", savedReport.getTableName()), logPair("storageReference", storageReference));
+ runBackendStepInput.getAsyncJobCallback().updateStatus("Generating Report");
+
+ //////////////////////////////////////////////////////////////////
+ // insert a rendered-report record indicating that it's running //
+ //////////////////////////////////////////////////////////////////
+ renderedReportRecord = new InsertAction().execute(new InsertInput(RenderedReport.TABLE_NAME).withRecordEntity(new RenderedReport()
+ .withSavedReportId(savedReport.getId())
+ .withStartTime(Instant.now())
+ // todo .withJobUuid(runBackendStepInput.get)
+ .withRenderedReportStatusId(RenderedReportStatus.RUNNING.getId())
+ .withReportFormat(ReportFormatPossibleValueEnum.valueOf(reportFormat.name()).getPossibleValueId())
+ )).getRecords().get(0);
+
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ // convert the report record to report meta-data, which the GenerateReportAction works on //
+ ////////////////////////////////////////////////////////////////////////////////////////////
+ QReportMetaData reportMetaData = new SavedReportToReportMetaDataAdapter().adapt(savedReport, reportFormat);
+
+ /////////////////////////////////////
+ // setup & run the generate action //
+ /////////////////////////////////////
+ ReportInput reportInput = new ReportInput();
+ reportInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
+ reportInput.setReportMetaData(reportMetaData);
+ reportInput.setReportDestination(new ReportDestination()
+ .withReportFormat(reportFormat)
+ .withReportOutputStream(outputStream));
+
+ Map values = runBackendStepInput.getValues();
+ reportInput.setInputValues(values);
+
+ ReportOutput reportOutput = new GenerateReportAction().execute(reportInput);
+
+ ///////////////////////////////////
+ // update record to show success //
+ ///////////////////////////////////
+ new UpdateAction().execute(new UpdateInput(RenderedReport.TABLE_NAME).withRecord(new QRecord()
+ .withValue("id", renderedReportRecord.getValue("id"))
+ .withValue("resultPath", storageReference)
+ .withValue("renderedReportStatusId", RenderedReportStatus.COMPLETE.getPossibleValueId())
+ .withValue("endTime", Instant.now())
+ .withValue("rowCount", reportOutput.getTotalRecordCount())
+ ));
+
+ runBackendStepOutput.addValue("downloadFileName", downloadFileBaseName + "." + reportFormat.getExtension());
+ runBackendStepOutput.addValue("storageTableName", storageTableName);
+ runBackendStepOutput.addValue("storageReference", storageReference);
+ LOG.info("Completed rendering a report", logPair("savedReportId", savedReport.getId()), logPair("tableName", savedReport.getTableName()), logPair("storageReference", storageReference), logPair("rowCount", reportOutput.getTotalRecordCount()));
+ }
+ catch(Exception e)
+ {
+ if(renderedReportRecord != null)
+ {
+ new UpdateAction().execute(new UpdateInput(RenderedReport.TABLE_NAME).withRecord(new QRecord()
+ .withValue("id", renderedReportRecord.getValue("id"))
+ .withValue("renderedReportStatusId", RenderedReportStatus.FAILED.getPossibleValueId())
+ .withValue("endTime", Instant.now())
+ .withValue("errorMessage", ExceptionUtils.concatenateMessagesFromChain(e))
+ ));
+ }
+
+ LOG.warn("Error rendering saved report", e);
+ throw (e);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String getDownloadFileBaseName(RunBackendStepInput runBackendStepInput, SavedReport report)
+ {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmm").withZone(ZoneId.systemDefault());
+ String datePart = formatter.format(Instant.now());
+
+ String downloadFileBaseName = runBackendStepInput.getValueString("downloadFileBaseName");
+ if(!StringUtils.hasContent(downloadFileBaseName))
+ {
+ downloadFileBaseName = report.getLabel();
+ }
+
+ //////////////////////////////////////////////////
+ // these chars have caused issues, so, disallow //
+ //////////////////////////////////////////////////
+ downloadFileBaseName = downloadFileBaseName.replaceAll("/", "-").replaceAll(",", "_");
+
+ return (downloadFileBaseName + " - " + datePart);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
new file mode 100644
index 00000000..f63d914f
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
@@ -0,0 +1,90 @@
+/*
+ * 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.processes.implementations.savedreports;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
+
+
+/*******************************************************************************
+ ** define process for rendering saved reports!
+ *******************************************************************************/
+public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterface
+{
+ public static final String NAME = "renderSavedReport";
+
+ public static final String FIELD_NAME_STORAGE_TABLE_NAME = "storageTableName";
+ public static final String FIELD_NAME_REPORT_FORMAT = "reportFormat";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QProcessMetaData produce(QInstance qInstance) throws QException
+ {
+ QProcessMetaData process = new QProcessMetaData()
+ .withName(NAME)
+ .withLabel("Render Report")
+ .withTableName(SavedReport.TABLE_NAME)
+ .withIcon(new QIcon().withName("print"))
+ .addStep(new QBackendStepMetaData()
+ .withName("pre")
+ .withInputData(new QFunctionInputMetaData()
+ .withField(new QFieldMetaData(FIELD_NAME_STORAGE_TABLE_NAME, QFieldType.STRING))
+ .withRecordListMetaData(new QRecordListMetaData().withTableName(SavedReport.TABLE_NAME)))
+ .withCode(new QCodeReference(RenderSavedReportPreStep.class)))
+ .addStep(new QFrontendStepMetaData()
+ .withName("input")
+ .withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
+ .withFormField(new QFieldMetaData(FIELD_NAME_REPORT_FORMAT, QFieldType.STRING)
+ .withPossibleValueSourceName(ReportFormatPossibleValueEnum.NAME)
+ .withIsRequired(true)))
+ .addStep(new QBackendStepMetaData()
+ .withName("execute")
+ .withInputData(new QFunctionInputMetaData().withRecordListMetaData(new QRecordListMetaData()
+ .withTableName(SavedReport.TABLE_NAME)))
+ .withCode(new QCodeReference(RenderSavedReportExecuteStep.class)))
+ .addStep(new QFrontendStepMetaData()
+ .withName("output")
+ .withComponent(new QFrontendComponentMetaData().withType(QComponentType.DOWNLOAD_FORM)));
+
+ return (process);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportPreStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportPreStep.java
new file mode 100644
index 00000000..f74298fb
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportPreStep.java
@@ -0,0 +1,79 @@
+/*
+ * 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.processes.implementations.savedreports;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+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.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ ** initial backend-step before rendering a saved report. does some basic
+ ** validations, and then (in future) will set up input fields (how??) for the
+ ** input screen.
+ *******************************************************************************/
+public class RenderSavedReportPreStep implements BackendStep
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ String storageTableName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_TABLE_NAME);
+ if(!StringUtils.hasContent(storageTableName))
+ {
+ throw (new QUserFacingException("Process configuration error: Missing value for storageTableName."));
+ }
+
+ if(QContext.getQInstance().getTable(storageTableName) == null)
+ {
+ throw (new QUserFacingException("Process configuration error: Unrecognized value for storageTableName - no table named [" + storageTableName + "] was found in the instance."));
+ }
+
+ List records = runBackendStepInput.getRecords();
+ if(!CollectionUtils.nullSafeHasContents(records))
+ {
+ throw (new QUserFacingException("No report was selected or found to be rendered."));
+ }
+
+ if(records.size() > 1)
+ {
+ throw (new QUserFacingException("You may only render 1 report at a time."));
+ }
+
+ SavedReport savedReport = new SavedReport(records.get(0));
+
+ // todo - check for inputs - set up the input screen...
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java
new file mode 100644
index 00000000..55ab7270
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/SavedReportToReportMetaDataAdapter.java
@@ -0,0 +1,408 @@
+/*
+ * 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.processes.implementations.savedreports;
+
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy;
+import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue;
+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.QueryJoin;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
+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.ExposedJoin;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.savedreports.ReportColumn;
+import com.kingsrook.qqq.backend.core.model.savedreports.ReportColumns;
+import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** GenerateReportAction takes in ReportMetaData.
+ **
+ ** This class knows how to adapt from a SavedReport to a ReportMetaData, so that
+ ** we can render a saved report.
+ *******************************************************************************/
+public class SavedReportToReportMetaDataAdapter
+{
+ private static final QLogger LOG = QLogger.getLogger(SavedReportToReportMetaDataAdapter.class);
+
+ private static Consumer jsonMapperCustomizer = om -> om.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QReportMetaData adapt(SavedReport savedReport, ReportFormat reportFormat) throws QException
+ {
+ try
+ {
+ QInstance qInstance = QContext.getQInstance();
+
+ QReportMetaData reportMetaData = new QReportMetaData();
+ reportMetaData.setName("savedReport:" + savedReport.getId());
+ reportMetaData.setLabel(savedReport.getLabel());
+
+ /////////////////////////////////////////////////////
+ // set up the data-source - e.g., table and filter //
+ /////////////////////////////////////////////////////
+ QReportDataSource dataSource = new QReportDataSource();
+ reportMetaData.setDataSources(List.of(dataSource));
+ dataSource.setName("main");
+
+ QTableMetaData table = qInstance.getTable(savedReport.getTableName());
+ dataSource.setSourceTable(savedReport.getTableName());
+ dataSource.setQueryFilter(getQQueryFilter(savedReport.getQueryFilterJson()));
+
+ //////////////////////////
+ // set up the main view //
+ //////////////////////////
+ QReportView view = new QReportView();
+ reportMetaData.setViews(ListBuilder.of(view));
+ view.setName("main");
+ view.setType(ReportType.TABLE);
+ view.setDataSourceName(dataSource.getName());
+ view.setLabel(savedReport.getLabel());
+ view.setIncludeHeaderRow(true);
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // columns in the saved-report should look like a serialized version of ReportColumns object //
+ // map them to a list of QReportField objects //
+ // also keep track of what joinTables we find that we need to select //
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ ReportColumns columnsObject = getReportColumns(savedReport.getColumnsJson());
+
+ List reportColumns = new ArrayList<>();
+ view.setColumns(reportColumns);
+
+ Set neededJoinTables = new HashSet<>();
+
+ for(ReportColumn column : columnsObject.extractVisibleColumns())
+ {
+ ////////////////////////////////////////////////////
+ // figure out the field being named by the column //
+ ////////////////////////////////////////////////////
+ String fieldName = column.getName();
+ FieldAndJoinTable fieldAndJoinTable = getField(savedReport, fieldName, qInstance, neededJoinTables, table);
+ if(fieldAndJoinTable == null)
+ {
+ continue;
+ }
+
+ //////////////////////////////////////////////////
+ // make a QReportField based on the table field //
+ //////////////////////////////////////////////////
+ reportColumns.add(makeQReportField(fieldName, fieldAndJoinTable));
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // set up joins, if we need any //
+ // note - test coverage here is provided by RDBMS module's GenerateReportActionRDBMSTest //
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ if(!neededJoinTables.isEmpty())
+ {
+ List queryJoins = new ArrayList<>();
+ dataSource.setQueryJoins(queryJoins);
+
+ for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
+ {
+ if(neededJoinTables.contains(exposedJoin.getJoinTable()))
+ {
+ QueryJoin queryJoin = new QueryJoin(exposedJoin.getJoinTable())
+ .withSelect(true)
+ .withType(QueryJoin.Type.LEFT)
+ .withBaseTableOrAlias(null)
+ .withAlias(null);
+
+ if(exposedJoin.getJoinPath().size() == 1)
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // Note, this is similar logic (and comment) in QFMD ... //
+ // todo - what about a join with a longer path? it would be nice to pass such joinNames through there too, //
+ // but what, that would actually be multiple queryJoins? needs a fair amount of thought. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ queryJoin.setJoinMetaData(qInstance.getJoin(exposedJoin.getJoinPath().get(0)));
+ }
+
+ queryJoins.add(queryJoin);
+ }
+ }
+ }
+
+ /////////////////////////////////////////
+ // if it's a pivot report, handle that //
+ /////////////////////////////////////////
+ if(StringUtils.hasContent(savedReport.getPivotTableJson()))
+ {
+ PivotTableDefinition pivotTableDefinition = getPivotTableDefinition(savedReport.getPivotTableJson());
+
+ QReportView pivotView = new QReportView();
+ reportMetaData.getViews().add(pivotView);
+ pivotView.setName("pivot");
+ pivotView.setLabel("Pivot Table");
+
+ if(reportFormat != null && reportFormat.getSupportsNativePivotTables())
+ {
+ pivotView.setType(ReportType.PIVOT);
+ pivotView.setPivotTableSourceViewName(view.getName());
+ pivotView.setPivotTableDefinition(pivotTableDefinition);
+ }
+ else
+ {
+ if(!CollectionUtils.nullSafeHasContents(pivotTableDefinition.getRows()))
+ {
+ throw (new QUserFacingException("To generate a pivot report in " + reportFormat + " format, it must have 1 or more Pivot Rows"));
+ }
+
+ if(CollectionUtils.nullSafeHasContents(pivotTableDefinition.getColumns()))
+ {
+ throw (new QUserFacingException("To generate a pivot report in " + reportFormat + " format, it may not have any Pivot Columns"));
+ }
+
+ ///////////////////////
+ // handle pivot rows //
+ ///////////////////////
+ List summaryFields = new ArrayList<>();
+ List summaryOrderByFields = new ArrayList<>();
+ for(PivotTableGroupBy row : pivotTableDefinition.getRows())
+ {
+ String fieldName = row.getFieldName();
+ FieldAndJoinTable fieldAndJoinTable = getField(savedReport, fieldName, qInstance, neededJoinTables, table);
+ if(fieldAndJoinTable == null)
+ {
+ LOG.warn("The field for a Pivot Row wasn't found, when converting to a summary...", logPair("savedReportId", savedReport.getId()), logPair("fieldName", fieldName));
+ continue;
+ }
+ summaryFields.add(fieldName);
+ summaryOrderByFields.add(new QFilterOrderBy(fieldName));
+ }
+
+ /////////////////////////
+ // handle pivot values //
+ /////////////////////////
+ List summaryViewColumns = new ArrayList<>();
+ for(PivotTableValue value : pivotTableDefinition.getValues())
+ {
+ String fieldName = value.getFieldName();
+ FieldAndJoinTable fieldAndJoinTable = getField(savedReport, fieldName, qInstance, neededJoinTables, table);
+ if(fieldAndJoinTable == null)
+ {
+ LOG.warn("The field for a Pivot Value wasn't found, when converting to a summary...", logPair("savedReportId", savedReport.getId()), logPair("fieldName", fieldName));
+ continue;
+ }
+
+ QReportField reportField = makeQReportField(fieldName, fieldAndJoinTable);
+ reportField.setName(fieldName + "_" + value.getFunction().name());
+ reportField.setLabel(StringUtils.ucFirst(value.getFunction().name().toLowerCase()) + " Of " + reportField.getLabel());
+ reportField.setFormula("${pivot." + value.getFunction().name().toLowerCase() + "." + fieldName + "}");
+ summaryViewColumns.add(reportField);
+ summaryOrderByFields.add(new QFilterOrderBy(reportField.getName()));
+ }
+
+ pivotView.setType(ReportType.SUMMARY);
+ pivotView.setDataSourceName(dataSource.getName());
+ pivotView.setIncludeHeaderRow(true);
+ pivotView.setIncludeTotalRow(true);
+ pivotView.setColumns(summaryViewColumns);
+ pivotView.setSummaryFields(summaryFields);
+ pivotView.withOrderByFields(summaryOrderByFields);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////
+ // in case the reportFormat doesn't support multiple views, and we have a pivot - //
+ // then remove the data view //
+ ////////////////////////////////////////////////////////////////////////////////////
+ if(reportFormat != null && !reportFormat.getSupportsMultipleViews())
+ {
+ reportMetaData.getViews().remove(0);
+ }
+ }
+
+ /////////////////////////////////////////////////////
+ // add input fields, if they're in the savedReport //
+ /////////////////////////////////////////////////////
+ if(StringUtils.hasContent(savedReport.getInputFieldsJson()))
+ {
+ ////////////////////////////////////
+ // todo turn on when implementing //
+ ////////////////////////////////////
+ // reportMetaData.setInputFields(JsonUtils.toObject(savedReport.getInputFieldsJson(), new TypeReference<>() {}), objectMapperConsumer);
+ throw (new IllegalStateException("Input Fields are not yet implemented"));
+ }
+
+ return (reportMetaData);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error adapting savedReport to reportMetaData", e, logPair("savedReportId", savedReport.getId()));
+ throw (new QException("Error adapting savedReport to reportMetaData", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static PivotTableDefinition getPivotTableDefinition(String pivotTableJson) throws IOException
+ {
+ return JsonUtils.toObject(pivotTableJson, PivotTableDefinition.class, jsonMapperCustomizer);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static ReportColumns getReportColumns(String columnsJson) throws IOException
+ {
+ return JsonUtils.toObject(columnsJson, ReportColumns.class, jsonMapperCustomizer);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QQueryFilter getQQueryFilter(String queryFilterJson) throws IOException
+ {
+ return JsonUtils.toObject(queryFilterJson, QQueryFilter.class, jsonMapperCustomizer);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static QReportField makeQReportField(String fieldName, FieldAndJoinTable fieldAndJoinTable)
+ {
+ QReportField reportField = new QReportField();
+
+ reportField.setName(fieldName);
+
+ if(fieldAndJoinTable.joinTable() == null)
+ {
+ ////////////////////////////////////////////////////////////
+ // for fields from this table, just use the field's label //
+ ////////////////////////////////////////////////////////////
+ reportField.setLabel(fieldAndJoinTable.field().getLabel());
+ }
+ else
+ {
+ ///////////////////////////////////////////////////////////////
+ // for fields from join tables, use table label: field label //
+ ///////////////////////////////////////////////////////////////
+ reportField.setLabel(fieldAndJoinTable.joinTable().getLabel() + ": " + fieldAndJoinTable.field().getLabel());
+ }
+
+ if(StringUtils.hasContent(fieldAndJoinTable.field().getPossibleValueSourceName()))
+ {
+ reportField.setShowPossibleValueLabel(true);
+ }
+
+ reportField.setType(fieldAndJoinTable.field().getType());
+
+ return reportField;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static FieldAndJoinTable getField(SavedReport savedReport, String fieldName, QInstance qInstance, Set neededJoinTables, QTableMetaData table)
+ {
+ QFieldMetaData field;
+ if(fieldName.contains("."))
+ {
+ String joinTableName = fieldName.replaceAll("\\..*", "");
+ String joinFieldName = fieldName.replaceAll(".*\\.", "");
+
+ QTableMetaData joinTable = qInstance.getTable(joinTableName);
+ if(joinTable == null)
+ {
+ LOG.warn("Saved Report has an unrecognized join table name", logPair("savedReportId", savedReport.getId()), logPair("joinTable", joinTable), logPair("fieldName", fieldName));
+ return null;
+ }
+
+ neededJoinTables.add(joinTableName);
+
+ field = joinTable.getFields().get(joinFieldName);
+ if(field == null)
+ {
+ LOG.warn("Saved Report has an unrecognized join field name", logPair("savedReportId", savedReport.getId()), logPair("fieldName", fieldName));
+ return null;
+ }
+
+ return new FieldAndJoinTable(field, joinTable);
+ }
+ else
+ {
+ field = table.getFields().get(fieldName);
+ if(field == null)
+ {
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // frontend may often pass __checked__ (or maybe other __ prefixes in the future - so - don't warn that. //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(!fieldName.startsWith("__"))
+ {
+ LOG.warn("Saved Report has an unexpected unrecognized field name", logPair("savedReportId", savedReport.getId()), logPair("table", table.getName()), logPair("fieldName", fieldName));
+ }
+ return null;
+ }
+
+ return new FieldAndJoinTable(field, null);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable) {}
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/AbstractTableSyncTransformStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/AbstractTableSyncTransformStep.java
index 55734984..a2b83ac6 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/AbstractTableSyncTransformStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/tablesync/AbstractTableSyncTransformStep.java
@@ -77,14 +77,14 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
private ProcessSummaryLine okToUpdate = StandardProcessSummaryLineProducer.getOkToUpdateLine();
private ProcessSummaryLine willNotInsert = new ProcessSummaryLine(Status.INFO)
- .withMessageSuffix("because of this process' configuration.")
+ .withMessageSuffix("because this process is not configured to insert records.")
.withSingularFutureMessage("will not be inserted ")
.withPluralFutureMessage("will not be inserted ")
.withSingularPastMessage("was not inserted ")
.withPluralPastMessage("were not inserted ");
private ProcessSummaryLine willNotUpdate = new ProcessSummaryLine(Status.INFO)
- .withMessageSuffix("because of this process' configuration.")
+ .withMessageSuffix("because this process is not configured to update records.")
.withSingularFutureMessage("will not be updated ")
.withPluralFutureMessage("will not be updated ")
.withSingularPastMessage("was not updated ")
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/state/TempFileStateProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/state/TempFileStateProvider.java
index 02ab06a9..77944945 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/state/TempFileStateProvider.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/state/TempFileStateProvider.java
@@ -26,6 +26,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Serializable;
+import java.nio.file.NoSuchFileException;
import java.time.Instant;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@@ -99,14 +100,14 @@ public class TempFileStateProvider implements StateProviderInterface
String json = FileUtils.readFileToString(getFile(key));
return (Optional.of(JsonUtils.toObject(json, type)));
}
- catch(FileNotFoundException fnfe)
+ catch(FileNotFoundException | NoSuchFileException fnfe)
{
return (Optional.empty());
}
catch(IOException e)
{
LOG.error("Error getting state from file", e);
- throw (new RuntimeException("Error retreiving state", e));
+ throw (new RuntimeException("Error retrieving state", e));
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/LocalMacDevUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/LocalMacDevUtils.java
new file mode 100644
index 00000000..d2725045
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/LocalMacDevUtils.java
@@ -0,0 +1,77 @@
+/*
+ * 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.utils;
+
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+
+
+/*******************************************************************************
+ ** Useful things to do on a mac, when doing development - that we can expect
+ ** may not exist in a prod or even CI environment. So, they'll only happen if
+ ** flags are set to do them, and if we're on a mac (e.g., paths exist)
+ *******************************************************************************/
+public class LocalMacDevUtils
+{
+ private static final QLogger LOG = QLogger.getLogger(LocalMacDevUtils.class);
+
+ public static boolean mayOpenFiles = false;
+
+ private static final String OPEN_PROGRAM_PATH = "/usr/bin/open";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void openFile(String path) throws IOException
+ {
+ if(mayOpenFiles && Files.exists(Path.of(OPEN_PROGRAM_PATH)))
+ {
+ Runtime.getRuntime().exec(new String[] { OPEN_PROGRAM_PATH, path });
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void openFile(String path, String appPath) throws IOException
+ {
+ if(mayOpenFiles && Files.exists(Path.of(OPEN_PROGRAM_PATH)))
+ {
+ if(Files.exists(Path.of(appPath)))
+ {
+ Runtime.getRuntime().exec(new String[] { OPEN_PROGRAM_PATH, "-a", appPath, path });
+ }
+ else
+ {
+ LOG.warn("App at path [" + appPath + " was not found - file [" + path + "] will not be opened.");
+ }
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java
index 80901c44..0fa1566e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ObjectUtils.java
@@ -139,4 +139,28 @@ public class ObjectUtils
return (b);
}
+
+
+ /*******************************************************************************
+ ** Utility to test a chained unsafe expression CAN get to the end and return true.
+ **
+ ** e.g., instead of:
+ ** if(a && a.b && a.b.c && a.b.c.d)
+ ** we can do:
+ ** if(ifCan(() -> a.b.c.d))
+ **
+ ** Note - if the supplier returns null, that counts as false!
+ *******************************************************************************/
+ public static boolean ifCan(UnsafeSupplier supplier)
+ {
+ try
+ {
+ return supplier.get();
+ }
+ catch(Throwable t)
+ {
+ return (false);
+ }
+ }
+
}
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
index 074c2469..ee6b0a59 100644
--- 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
@@ -29,8 +29,12 @@ import java.math.BigDecimal;
/*******************************************************************************
** Classes that support doing data aggregations (e.g., count, sum, min, max, average).
** Sub-classes should supply the type parameter.
+ **
+ ** The AVG_T parameter describes the type used for the average getAverage method
+ ** which, e.g, for date types, might be a date, vs. numbers, they'd probably be
+ ** BigDecimal.
*******************************************************************************/
-public interface AggregatesInterface
+public interface AggregatesInterface
{
/*******************************************************************************
**
@@ -60,5 +64,51 @@ public interface AggregatesInterface
/*******************************************************************************
**
*******************************************************************************/
- BigDecimal getAverage();
+ AVG_T getAverage();
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default BigDecimal getProduct()
+ {
+ return (null);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default BigDecimal getVariance()
+ {
+ return (null);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default BigDecimal getVarP()
+ {
+ return (null);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default BigDecimal getStandardDeviation()
+ {
+ return (null);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default BigDecimal getStdDevP()
+ {
+ return (null);
+ }
+
}
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
index da7f1703..76a6f0d8 100644
--- 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
@@ -28,13 +28,16 @@ import java.math.BigDecimal;
/*******************************************************************************
** BigDecimal version of data aggregator
*******************************************************************************/
-public class BigDecimalAggregates implements AggregatesInterface
+public class BigDecimalAggregates implements AggregatesInterface
{
private int count = 0;
// private Integer countDistinct;
private BigDecimal sum;
private BigDecimal min;
private BigDecimal max;
+ private BigDecimal product;
+
+ private VarianceCalculator varianceCalculator = new VarianceCalculator();
@@ -59,6 +62,15 @@ public class BigDecimalAggregates implements AggregatesInterface
sum = sum.add(input);
}
+ if(product == null)
+ {
+ product = input;
+ }
+ else
+ {
+ product = product.multiply(input);
+ }
+
if(min == null || input.compareTo(min) < 0)
{
min = input;
@@ -68,6 +80,52 @@ public class BigDecimalAggregates implements AggregatesInterface
{
max = input;
}
+
+ varianceCalculator.updateVariance(input);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getVariance()
+ {
+ return (varianceCalculator.getVariance());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getVarP()
+ {
+ return (varianceCalculator.getVarP());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getStandardDeviation()
+ {
+ return (varianceCalculator.getStandardDeviation());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getStdDevP()
+ {
+ return (varianceCalculator.getStdDevP());
}
@@ -116,6 +174,18 @@ public class BigDecimalAggregates implements AggregatesInterface
+ /*******************************************************************************
+ ** Getter for product
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getProduct()
+ {
+ return product;
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/InstantAggregates.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/InstantAggregates.java
new file mode 100644
index 00000000..adb1a591
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/InstantAggregates.java
@@ -0,0 +1,136 @@
+/*
+ * 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.BigInteger;
+import java.time.Instant;
+
+
+/*******************************************************************************
+ ** Instant version of data aggregator
+ *******************************************************************************/
+public class InstantAggregates implements AggregatesInterface
+{
+ private int count = 0;
+ // private Integer countDistinct;
+
+ private BigInteger sumMillis = BigInteger.ZERO;
+
+ private Instant min;
+ private Instant max;
+
+
+
+ /*******************************************************************************
+ ** Add a new value to this aggregate set
+ *******************************************************************************/
+ public void add(Instant input)
+ {
+ if(input == null)
+ {
+ return;
+ }
+
+ count++;
+
+ sumMillis = sumMillis.add(new BigInteger(String.valueOf(input.toEpochMilli())));
+
+ 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 Instant getSum()
+ {
+ //////////////////////////////////////////
+ // sum of date-times doesn't make sense //
+ //////////////////////////////////////////
+ return (null);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Instant getMin()
+ {
+ return (min);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Instant getMax()
+ {
+ return (max);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public Instant getAverage()
+ {
+ if(this.count > 0)
+ {
+ BigInteger averageMillis = this.sumMillis.divide(new BigInteger(String.valueOf(count)));
+ if(averageMillis.compareTo(new BigInteger(String.valueOf(Long.MAX_VALUE))) < 0)
+ {
+ return (Instant.ofEpochMilli(averageMillis.longValue()));
+ }
+ }
+
+ 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
index 292e8a01..15efecea 100644
--- 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
@@ -28,13 +28,16 @@ import java.math.BigDecimal;
/*******************************************************************************
** Integer version of data aggregator
*******************************************************************************/
-public class IntegerAggregates implements AggregatesInterface
+public class IntegerAggregates implements AggregatesInterface
{
- private int count = 0;
+ private int count = 0;
// private Integer countDistinct;
- private Integer sum;
- private Integer min;
- private Integer max;
+ private Integer sum;
+ private Integer min;
+ private Integer max;
+ private BigDecimal product;
+
+ private VarianceCalculator varianceCalculator = new VarianceCalculator();
@@ -48,6 +51,8 @@ public class IntegerAggregates implements AggregatesInterface
return;
}
+ BigDecimal inputBD = new BigDecimal(input);
+
count++;
if(sum == null)
@@ -59,6 +64,15 @@ public class IntegerAggregates implements AggregatesInterface
sum = sum + input;
}
+ if(product == null)
+ {
+ product = inputBD;
+ }
+ else
+ {
+ product = product.multiply(inputBD);
+ }
+
if(min == null || input < min)
{
min = input;
@@ -68,6 +82,52 @@ public class IntegerAggregates implements AggregatesInterface
{
max = input;
}
+
+ varianceCalculator.updateVariance(inputBD);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getVariance()
+ {
+ return (varianceCalculator.getVariance());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getVarP()
+ {
+ return (varianceCalculator.getVarP());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getStandardDeviation()
+ {
+ return (varianceCalculator.getStandardDeviation());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getStdDevP()
+ {
+ return (varianceCalculator.getStdDevP());
}
@@ -116,6 +176,18 @@ public class IntegerAggregates implements AggregatesInterface
+ /*******************************************************************************
+ ** Getter for product
+ **
+ *******************************************************************************/
+ @Override
+ public BigDecimal getProduct()
+ {
+ return product;
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LocalDateAggregates.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LocalDateAggregates.java
new file mode 100644
index 00000000..3c64e200
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LocalDateAggregates.java
@@ -0,0 +1,136 @@
+/*
+ * 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.BigInteger;
+import java.time.LocalDate;
+
+
+/*******************************************************************************
+ ** LocalDate version of data aggregator
+ *******************************************************************************/
+public class LocalDateAggregates implements AggregatesInterface
+{
+ private int count = 0;
+ // private Integer countDistinct;
+
+ private BigInteger sumMillis = BigInteger.ZERO;
+
+ private LocalDate min;
+ private LocalDate max;
+
+
+
+ /*******************************************************************************
+ ** Add a new value to this aggregate set
+ *******************************************************************************/
+ public void add(LocalDate input)
+ {
+ if(input == null)
+ {
+ return;
+ }
+
+ count++;
+
+ sumMillis = sumMillis.add(new BigInteger(String.valueOf(input.toEpochDay())));
+
+ 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 LocalDate getSum()
+ {
+ //////////////////////////////////////////
+ // sum of date-times doesn't make sense //
+ //////////////////////////////////////////
+ return (null);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public LocalDate getMin()
+ {
+ return (min);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public LocalDate getMax()
+ {
+ return (max);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public LocalDate getAverage()
+ {
+ if(this.count > 0)
+ {
+ BigInteger averageEpochDay = this.sumMillis.divide(new BigInteger(String.valueOf(count)));
+ if(averageEpochDay.compareTo(new BigInteger(String.valueOf(Long.MAX_VALUE))) < 0)
+ {
+ return (LocalDate.ofEpochDay(averageEpochDay.longValue()));
+ }
+ }
+
+ return (null);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LongAggregates.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LongAggregates.java
index bcf1862b..e131cda1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LongAggregates.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/LongAggregates.java
@@ -28,13 +28,16 @@ import java.math.BigDecimal;
/*******************************************************************************
** Long version of data aggregator
*******************************************************************************/
-public class LongAggregates implements AggregatesInterface
+public class LongAggregates implements AggregatesInterface
{
private int count = 0;
// private Long countDistinct;
private Long sum;
private Long min;
private Long max;
+ private BigDecimal product;
+
+ private VarianceCalculator varianceCalculator = new VarianceCalculator();
@@ -48,6 +51,8 @@ public class LongAggregates implements AggregatesInterface
return;
}
+ BigDecimal inputBD = new BigDecimal(input);
+
count++;
if(sum == null)
@@ -59,6 +64,15 @@ public class LongAggregates implements AggregatesInterface
sum = sum + input;
}
+ if(product == null)
+ {
+ product = inputBD;
+ }
+ else
+ {
+ product = product.multiply(inputBD);
+ }
+
if(min == null || input < min)
{
min = input;
@@ -68,6 +82,52 @@ public class LongAggregates implements AggregatesInterface