Merge pull request #79 from Kingsrook/feature/CE-881-create-basic-saved-reports

Feature/ce 881 create basic saved reports
This commit is contained in:
2024-04-17 18:55:53 -05:00
committed by GitHub
127 changed files with 10689 additions and 539 deletions

View File

@ -102,6 +102,16 @@
<artifactId>fastexcel</artifactId>
<version>0.12.15</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0</artifactId>

View File

@ -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
{

View File

@ -303,7 +303,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
}
@ -329,8 +329,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(table, oldRecord, fieldName, field, oldValue);
if(oldValue == null)
{
@ -464,7 +464,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/*******************************************************************************
**
*******************************************************************************/
private static String getFormattedValueForAuditDetail(QRecord record, String fieldName, QFieldMetaData field, Serializable value)
private static String getFormattedValueForAuditDetail(QTableMetaData table, QRecord record, String fieldName, QFieldMetaData field, Serializable value)
{
String formattedValue = null;
if(value != null)
@ -479,7 +479,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
formattedValue = QValueFormatter.formatValue(field, value);
QValueFormatter.setDisplayValuesInRecord(table, table.getFields(), record);
formattedValue = record.getDisplayValue(fieldName);
}
}

View File

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

View File

@ -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))));
}
}

View File

@ -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<QFieldMetaData> fields, String label) throws QReportingException
public void start(ExportInput exportInput, List<QFieldMetaData> 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();
}

View File

@ -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<QFieldMetaData> fields = getFields(exportInput);
reportStreamer.start(exportInput, fields, "Sheet 1");
//////////////////////////////////////////////////////////
// it seems we can pass a view with just a name in here //
//////////////////////////////////////////////////////////
List<QReportView> 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)
{

View File

@ -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<QFieldMetaData> fields, String label) throws QReportingException;
/*******************************************************************************
** Called as records flow into the pipe.
******************************************************************************/
void addRecords(List<QRecord> 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<QReportView> 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<QFieldMetaData> fields, String label, QReportView view) throws QReportingException;
/*******************************************************************************
** Called as records flow into the pipe.
******************************************************************************/
void addRecords(List<QRecord> 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;
}

View File

@ -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<ReportInput, ReportOutput>
{
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<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> summaryAggregates = new HashMap<>();
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> varianceAggregates = new HashMap<>();
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> summaryAggregates = new HashMap<>();
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> varianceAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?, ?>> totalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?, ?>> varianceTotalAggregates = new HashMap<>();
private QReportMetaData report;
private ReportFormat reportFormat;
private ExportStreamerInterface reportStreamer;
private List<QReportDataSource> dataSources;
private List<QReportView> views;
private Map<String, Integer> 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<QReportView> dataSourceTableViews = report.getViews().stream()
List<QReportView> dataSourceTableViews = views.stream()
.filter(v -> v.getType().equals(ReportType.TABLE))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
List<QReportView> dataSourceSummaryViews = report.getViews().stream()
List<QReportView> dataSourceSummaryViews = views.stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
List<QReportView> dataSourceVariantViews = report.getViews().stream()
List<QReportView> 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<QFieldMetaData> 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<QReportView> summaryViews, List<QReportView> 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<QReportView> summaryViews, List<QReportView> 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<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext)
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext) throws QException
{
Set<String> 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<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> aggregatesMap)
private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> aggregatesMap) throws QException
{
Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> 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<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates, SummaryKey key)
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates, SummaryKey key) throws QException
{
Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
Map<String, AggregatesInterface<?, ?>> 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<String, AggregatesInterface<?>> aggregatesMap)
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?, ?>> 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<Integer> fieldAggregates = (AggregatesInterface<Integer>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
fieldAggregates.add(record.getValueInteger(field.getName()));
AggregatesInterface<String, ?> fieldAggregates = (AggregatesInterface<String, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new StringAggregates());
fieldAggregates.add(record.getDisplayValue(fieldName));
}
else if(field.getType().equals(QFieldType.INTEGER))
{
@SuppressWarnings("unchecked")
AggregatesInterface<Integer, ?> fieldAggregates = (AggregatesInterface<Integer, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new IntegerAggregates());
fieldAggregates.add(record.getValueInteger(fieldName));
}
else if(field.getType().equals(QFieldType.LONG))
{
@SuppressWarnings("unchecked")
AggregatesInterface<Long> fieldAggregates = (AggregatesInterface<Long>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new LongAggregates());
fieldAggregates.add(record.getValueLong(field.getName()));
AggregatesInterface<Long, ?> fieldAggregates = (AggregatesInterface<Long, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new LongAggregates());
fieldAggregates.add(record.getValueLong(fieldName));
}
else if(field.getType().equals(QFieldType.DECIMAL))
{
@SuppressWarnings("unchecked")
AggregatesInterface<BigDecimal> fieldAggregates = (AggregatesInterface<BigDecimal>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
fieldAggregates.add(record.getValueBigDecimal(field.getName()));
AggregatesInterface<BigDecimal, ?> fieldAggregates = (AggregatesInterface<BigDecimal, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new BigDecimalAggregates());
fieldAggregates.add(record.getValueBigDecimal(fieldName));
}
else if(field.getType().equals(QFieldType.DATE_TIME))
{
@SuppressWarnings("unchecked")
AggregatesInterface<Instant, ?> fieldAggregates = (AggregatesInterface<Instant, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new InstantAggregates());
fieldAggregates.add(record.getValueInstant(fieldName));
}
else if(field.getType().equals(QFieldType.DATE))
{
@SuppressWarnings("unchecked")
AggregatesInterface<LocalDate, ?> fieldAggregates = (AggregatesInterface<LocalDate, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new LocalDateAggregates());
fieldAggregates.add(record.getValueLocalDate(fieldName));
}
if(field.getType().isStringLike())
{
@SuppressWarnings("unchecked")
AggregatesInterface<String, ?> fieldAggregates = (AggregatesInterface<String, ?>) 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<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
List<QReportView> 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<QFieldMetaData> getFields(QTableMetaData table, QReportView view)
private List<QFieldMetaData> getFields(QTableMetaData table, QReportView view) throws QException
{
List<QFieldMetaData> 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<QRecord> summaryRows = new ArrayList<>();
for(Map.Entry<SummaryKey, Map<String, AggregatesInterface<?>>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
for(Map.Entry<SummaryKey, Map<String, AggregatesInterface<?, ?>>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
{
SummaryKey summaryKey = entry.getKey();
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
Map<String, Serializable> summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
SummaryKey summaryKey = entry.getKey();
Map<String, AggregatesInterface<?, ?>> fieldAggregates = entry.getValue();
Map<String, Serializable> summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
variableInterpreter.addValueMap("pivot", summaryValues);
variableInterpreter.addValueMap("summary", summaryValues);
@ -681,9 +878,9 @@ public class GenerateReportAction
if(!varianceAggregates.isEmpty())
{
Map<SummaryKey, Map<String, AggregatesInterface<?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
Map<String, Serializable> varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
Map<String, AggregatesInterface<?, ?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
Map<String, Serializable> 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<String, Serializable> getSummaryValuesForInterpreter(Map<String, AggregatesInterface<?>> fieldAggregates)
private Map<String, Serializable> getSummaryValuesForInterpreter(Map<String, AggregatesInterface<?, ?>> fieldAggregates)
{
Map<String, Serializable> summaryValuesForInterpreter = new HashMap<>();
for(Map.Entry<String, AggregatesInterface<?>> subEntry : fieldAggregates.entrySet())
for(Map.Entry<String, AggregatesInterface<?, ?>> 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());
}
}
}
}

View File

@ -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<QFieldMetaData> 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<String, String> fieldLabelMemoization = new Memoization<>();
@ -69,21 +85,124 @@ public class JsonExportStreamer implements ExportStreamerInterface
**
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
public void preRun(ReportDestination reportDestination, List<QReportView> 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<QFieldMetaData> 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<String, Serializable> 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<String> 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);
}
}
}

View File

@ -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<QFieldMetaData> fields, String label) throws QReportingException
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
{
this.exportInput = exportInput;
this.fields = fields;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QReportView> views, QReportView pivotTableView) throws QReportingException
{
Optional<QReportView> 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();
}
}

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
import org.dhatim.fastexcel.BorderSide;
@ -30,7 +30,7 @@ import org.dhatim.fastexcel.StyleSetter;
/*******************************************************************************
** Version of excel styler that does bold headers and footers, with basic borders.
*******************************************************************************/
public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
public class BoldHeaderAndFooterFastExcelStyler implements FastExcelStylerInterface
{
/*******************************************************************************
@ -60,6 +60,9 @@ public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public void styleTotalsRow(StyleSetter totalsRowStyle)
{

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
import java.io.OutputStream;
@ -34,8 +34,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.ExcelStylerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
@ -43,7 +43,9 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.dhatim.fastexcel.StyleSetter;
@ -52,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<QFieldMetaData> fields;
private OutputStream outputStream;
private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
private Map<String, String> excelCellFormats;
private FastExcelStylerInterface fastExcelStylerInterface = new PlainFastExcelStyler();
private Map<String, String> 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<QFieldMetaData> fields, String label) throws QReportingException
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
{
try
{
this.exportInput = exportInput;
this.fields = fields;
table = exportInput.getTable();
outputStream = this.exportInput.getReportOutputStream();
outputStream = this.exportInput.getReportDestination().getReportOutputStream();
this.row = 0;
this.sheetCount++;
@ -121,7 +123,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
/////////////////////////////////////////////////////////////////////////////////////////////////////
if(workbook == null)
{
String appName = "QQQ";
String appName = ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getBranding().getAppName(), "QQQ");
QInstance instance = exportInput.getInstance();
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
{
@ -167,7 +169,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
worksheet.range(row, 0, row, fields.size() - 1).merge();
StyleSetter titleStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
excelStylerInterface.styleTitleRow(titleStyle);
fastExcelStylerInterface.styleTitleRow(titleStyle);
titleStyle.set();
row++;
@ -187,7 +189,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
}
StyleSetter headerStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
excelStylerInterface.styleHeaderRow(headerStyle);
fastExcelStylerInterface.styleHeaderRow(headerStyle);
headerStyle.set();
row++;
@ -315,7 +317,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
writeRecord(record);
StyleSetter totalsRowStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
excelStylerInterface.styleTotalsRow(totalsRowStyle);
fastExcelStylerInterface.styleTotalsRow(totalsRowStyle);
totalsRowStyle.set();
}

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
import org.dhatim.fastexcel.StyleSetter;
@ -29,7 +29,7 @@ import org.dhatim.fastexcel.StyleSetter;
** Interface for classes that know how to apply styles to an Excel stream being
** built by fastexcel.
*******************************************************************************/
public interface ExcelStylerInterface
public interface FastExcelStylerInterface
{
/*******************************************************************************

View File

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

View File

@ -0,0 +1,93 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/*******************************************************************************
** Version of POI excel styler that does bold headers and footers, with basic borders.
*******************************************************************************/
public class BoldHeaderAndFooterPoiExcelStyler implements PoiExcelStylerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public XSSFCellStyle createStyleForTitle(XSSFWorkbook workbook, CreationHelper createHelper)
{
Font font = workbook.createFont();
font.setFontHeightInPoints((short) 14);
font.setBold(true);
XSSFCellStyle cellStyle = workbook.createCellStyle();
cellStyle.setFont(font);
cellStyle.setAlignment(HorizontalAlignment.CENTER);
return (cellStyle);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
{
Font font = workbook.createFont();
font.setBold(true);
XSSFCellStyle cellStyle = workbook.createCellStyle();
cellStyle.setFont(font);
cellStyle.setBorderBottom(BorderStyle.THIN);
return (cellStyle);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public XSSFCellStyle createStyleForFooter(XSSFWorkbook workbook, CreationHelper createHelper)
{
Font font = workbook.createFont();
font.setBold(true);
XSSFCellStyle cellStyle = workbook.createCellStyle();
cellStyle.setFont(font);
cellStyle.setBorderTop(BorderStyle.THIN);
cellStyle.setBorderBottom(BorderStyle.DOUBLE);
return (cellStyle);
}
}

View File

@ -0,0 +1,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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.Writer;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.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<QReportView> views;
private ExportInput exportInput;
private List<QFieldMetaData> 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<String, String> excelCellFormats;
private Map<String, XSSFCellStyle> styles = new HashMap<>();
private int rowNo = 0;
private int sheetIndex = 1;
private Map<String, String> pivotViewToCacheDefinitionReferenceMap = new HashMap<>();
private Writer activeSheetWriter = null;
private StreamedSheetWriter sheetWriter = null;
private QReportView currentView = null;
private Map<String, List<QFieldMetaData>> fieldsPerView = new HashMap<>();
private Map<String, Integer> rowsPerView = new HashMap<>();
private Map<String, String> labelViewsByName = new HashMap<>();
/*******************************************************************************
**
*******************************************************************************/
public ExcelPoiBasedStreamingExportStreamer()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void preRun(ReportDestination reportDestination, List<QReportView> views) throws QReportingException
{
try
{
this.outputStream = reportDestination.getReportOutputStream();
this.views = views;
///////////////////////////////////////////////////////////////////////////////
// create 'template' workbook through poi - with sheets corresponding to our //
// actual file this will be a zip file (stream), with entries for all of the //
// files in the final xlsx but without any data, so it'll be small //
///////////////////////////////////////////////////////////////////////////////
XSSFWorkbook workbook = new XSSFWorkbook();
createStyles(workbook);
//////////////////////////////////////////////////////////////////////////////////////////////////
// for each of the sheets, create it in the workbook, and put a reference to it in the sheetMap //
//////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, XSSFSheet> sheetMapByExcelReference = new HashMap<>();
Map<String, XSSFSheet> sheetMapByViewName = new HashMap<>();
int sheetCounter = 1;
for(QReportView view : views)
{
String label = Objects.requireNonNullElse(view.getLabel(), "Sheet " + sheetCounter);
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<String> pivotViewNames = new ArrayList<>();
for(QReportView view : views)
{
if(ReportType.PIVOT.equals(view.getType()))
{
pivotViewNames.add(view.getName());
XSSFSheet pivotTableSheet = Objects.requireNonNull(sheetMapByViewName.get(view.getName()), "Could not get pivot table sheet view by name: " + view.getName());
XSSFSheet dataSheet = Objects.requireNonNull(sheetMapByViewName.get(view.getPivotTableSourceViewName()), "Could not get pivot table source sheet by view name: " + view.getPivotTableSourceViewName());
QReportView dataView = ReportUtils.getSourceViewForPivotTableView(views, view);
createPivotTableTemplate(pivotTableSheet, view, dataSheet, dataView);
}
}
Iterator<String> pivotViewNameIterator = pivotViewNames.iterator();
/////////////////////////////////////////////////////////
// write that template worksheet zip out to byte array //
/////////////////////////////////////////////////////////
ByteArrayOutputStream templateBAOS = new ByteArrayOutputStream();
workbook.write(templateBAOS);
templateBAOS.close();
byte[] templateBytes = templateBAOS.toByteArray();
/////////////////////////////////////////////////////////////////////////////////////////////
// open up a zipOutputStream around the output stream that the report is to be written to. //
/////////////////////////////////////////////////////////////////////////////////////////////
this.zipOutputStream = new ZipOutputStream(this.outputStream);
/////////////////////////////////////////////////////////////////////////////////////////////////
// copy over all the entries in the template zip that aren't the sheets into the output stream //
/////////////////////////////////////////////////////////////////////////////////////////////////
ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(templateBytes));
ZipEntry zipTemplateEntry = null;
byte[] buffer = new byte[2048];
while((zipTemplateEntry = zipInputStream.getNextEntry()) != null)
{
if(zipTemplateEntry.getName().matches(".*/pivotCacheDefinition.*.xml"))
{
//////////////////////////////////////////////////////////////////////////////////////////////////////
// if this zip entry is a pivotCacheDefinition, then don't write it to the output stream right now. //
// instead, just map the pivot view's name to the zipTemplateEntry name //
//////////////////////////////////////////////////////////////////////////////////////////////////////
if(!pivotViewNameIterator.hasNext())
{
throw new QReportingException("Found a pivot cache definition [" + zipTemplateEntry.getName() + "] in the template ZIP, but no (more) corresponding pivot view names");
}
String pivotViewName = pivotViewNameIterator.next();
LOG.info("Holding on a pivot cache definition zip template entry [" + pivotViewName + "] [" + zipTemplateEntry.getName() + "]...");
pivotViewToCacheDefinitionReferenceMap.put(pivotViewName, zipTemplateEntry.getName());
}
else if(!sheetMapByExcelReference.containsKey(zipTemplateEntry.getName()))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, if we don't have this zipTemplateEntry name in our map of sheets, then this is a kinda "meta" //
// file that we don't really care about (e.g., not our sheet data), so just copy it to the output stream. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.info("Copying zip template entry [" + zipTemplateEntry.getName() + "] to output stream");
zipOutputStream.putNextEntry(new ZipEntry(zipTemplateEntry.getName()));
int length;
while((length = zipInputStream.read(buffer)) > 0)
{
zipOutputStream.write(buffer, 0, length);
}
zipInputStream.closeEntry();
}
else
{
////////////////////////////////////////////////////////////////////////////////////
// else - this is a sheet - so again, don't write it yet - stream its data below. //
////////////////////////////////////////////////////////////////////////////////////
LOG.info("Skipping presumed sheet zip template entry [" + zipTemplateEntry.getName() + "] to output stream");
}
}
zipInputStream.close();
}
catch(Exception e)
{
throw (new QReportingException("Error preparing to generate spreadsheet", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void createPivotTableTemplate(XSSFSheet pivotTableSheet, QReportView pivotView, XSSFSheet dataSheet, QReportView dataView) throws QReportingException
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// write just enough data to the dataView's sheet so that we can refer to it for creating the pivot table. //
// we need to do this, because POI will try to create the pivotCache referring to the data sheet, and if //
// there isn't any data there, it'll crash. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
XSSFRow headerRow = dataSheet.createRow(0);
int columnNo = 0;
for(QReportField column : dataView.getColumns())
{
XSSFCell cell = headerRow.createCell(columnNo++);
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<QReportField> 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<QReportField> columns, String fieldName) throws QReportingException
{
for(int i = 0; i < columns.size(); i++)
{
if(columns.get(i).getName().equals(fieldName))
{
return (i);
}
}
throw (new QReportingException("Could not find column by name [" + fieldName + "]"));
}
/*******************************************************************************
**
*******************************************************************************/
private void createStyles(XSSFWorkbook workbook)
{
CreationHelper createHelper = workbook.getCreationHelper();
XSSFCellStyle dateStyle = workbook.createCellStyle();
dateStyle.setDataFormat(createHelper.createDataFormat().getFormat(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<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
{
try
{
/////////////////////////////////////////
// close previous sheet if one is open //
/////////////////////////////////////////
closeLastSheetIfOpen();
if(currentView != null)
{
this.rowsPerView.put(currentView.getName(), rowNo);
}
this.currentView = view;
this.exportInput = exportInput;
this.fields = fields;
this.rowNo = 0;
this.fieldsPerView.put(view.getName(), fields);
//////////////////////////////////////////
// start the new sheet as: //
// - a new entry in the zipOutputStream //
// - with a new output stream writer //
// - and with a SpreadsheetWriter //
//////////////////////////////////////////
zipOutputStream.putNextEntry(new ZipEntry("xl/worksheets/sheet" + this.sheetIndex++ + ".xml"));
activeSheetWriter = new OutputStreamWriter(zipOutputStream);
sheetWriter = new 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<QRecord> qRecords) throws QReportingException
{
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
try
{
for(QRecord qRecord : qRecords)
{
writeRecord(qRecord);
}
}
catch(Exception e)
{
LOG.error("Exception generating excel file", e);
try
{
outputStream.close();
}
catch(IOException ex)
{
LOG.warn("Secondary error closing excel output stream", e);
}
throw (new QReportingException("Error generating Excel report", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writeRecord(QRecord qRecord) throws IOException
{
writeRecord(qRecord, false);
}
/*******************************************************************************
**
*******************************************************************************/
private void writeRecord(QRecord qRecord, boolean isFooter) throws IOException
{
sheetWriter.insertRow(rowNo++);
int styleIndex = -1;
int dateStyleIndex = styles.get("date").getIndex();
int dateTimeStyleIndex = styles.get("datetime").getIndex();
if(isFooter)
{
styleIndex = styles.get("footer").getIndex();
dateStyleIndex = styles.get("footer-date").getIndex();
dateTimeStyleIndex = styles.get("footer-datetime").getIndex();
}
int col = 0;
for(QFieldMetaData field : fields)
{
Serializable value = qRecord.getValue(field.getName());
if(value != null)
{
if(value instanceof String s)
{
sheetWriter.createCell(col, s, styleIndex);
}
else if(value instanceof Number n)
{
sheetWriter.createCell(col, n.doubleValue(), styleIndex);
if(excelCellFormats != null)
{
String format = excelCellFormats.get(field.getName());
if(format != null)
{
////////////////////////////////////////////////////////////////////////////////////////////
// todo - 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<String, String> displayFormats)
{
this.excelCellFormats = new HashMap<>();
for(Map.Entry<String, String> entry : displayFormats.entrySet())
{
String excelFormat = DisplayFormat.getExcelFormat(entry.getValue());
if(excelFormat != null)
{
excelCellFormats.put(entry.getKey(), excelFormat);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writePivotTable(QReportView pivotTableView, QReportView dataView) throws QReportingException
{
try
{
//////////////////////////////////////////////////////////////////////////////////
// write the xml file that is the pivot table sheet. //
// note that the ZipEntry here will have been started above in the start method //
//////////////////////////////////////////////////////////////////////////////////
activeSheetWriter.write("""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main">
<sheetPr>
<outlinePr summaryBelow="0" summaryRight="0"/>
</sheetPr>
<sheetViews>
<sheetView workbookViewId="0"/>
</sheetViews>
<sheetFormatPr customHeight="1" defaultColWidth="14.43" defaultRowHeight="15.0"/>
<sheetData>
<row r="1"/>
</sheetData>
</worksheet>
""");
activeSheetWriter.flush();
////////////////////////////////////////////////////////////////////////////
// start a new zip entry, for this pivot view's cacheDefinition reference //
////////////////////////////////////////////////////////////////////////////
zipOutputStream.putNextEntry(new ZipEntry(pivotViewToCacheDefinitionReferenceMap.get(pivotTableView.getName())));
/////////////////////////////////////////////////////////
// prepare the xml for each field (e.g., w/ its label) //
/////////////////////////////////////////////////////////
List<String> cachedFieldElements = new ArrayList<>();
for(QFieldMetaData column : this.fieldsPerView.get(dataView.getName()))
{
cachedFieldElements.add(String.format("""
<cacheField numFmtId="0" name="%s">
<sharedItems/>
</cacheField>
""", column.getLabel()));
}
/////////////////////////////////////////////////////////////////////////////////////
// write the xml file that is the pivot cache definition (structure only, no data) //
/////////////////////////////////////////////////////////////////////////////////////
activeSheetWriter = new OutputStreamWriter(zipOutputStream);
activeSheetWriter.write(String.format("""
<?xml version="1.0" encoding="UTF-8"?>
<pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" createdVersion="3" minRefreshableVersion="3" refreshedVersion="3" refreshedBy="Apache POI" refreshedDate="1.711395767702E12" refreshOnLoad="true" r:id="rId1">
<cacheSource type="worksheet">
<worksheetSource sheet="%s" ref="A1:%s%d"/>
</cacheSource>
<cacheFields count="%d">
%s
</cacheFields>
</pivotCacheDefinition>
""",
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());
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/*******************************************************************************
** 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);
}
}

View File

@ -0,0 +1,59 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/*******************************************************************************
** Interface for classes that know how to apply styles to an Excel stream being
** built by POI.
*******************************************************************************/
public interface PoiExcelStylerInterface
{
/*******************************************************************************
**
*******************************************************************************/
default XSSFCellStyle createStyleForTitle(XSSFWorkbook workbook, CreationHelper createHelper)
{
return (workbook.createCellStyle());
}
/*******************************************************************************
**
*******************************************************************************/
default XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
{
return (workbook.createCellStyle());
}
/*******************************************************************************
**
*******************************************************************************/
default XSSFCellStyle createStyleForFooter(XSSFWorkbook workbook, CreationHelper createHelper)
{
return (workbook.createCellStyle());
}
}

View File

@ -0,0 +1,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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
import java.io.IOException;
import java.io.Writer;
import java.util.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("""
<?xml version="1.0" encoding="UTF-8"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>""");
}
/*******************************************************************************
**
*******************************************************************************/
public void endSheet() throws IOException
{
writer.write("""
</sheetData>
</worksheet>""");
}
/*******************************************************************************
** Insert a new row
**
** @param rowNo 0-based row number
*******************************************************************************/
public void insertRow(int rowNo) throws IOException
{
writer.write("<row r=\"" + (rowNo + 1) + "\">\n");
this.rowNo = rowNo;
}
/*******************************************************************************
** Insert row end marker
*******************************************************************************/
public void endRow() throws IOException
{
writer.write("</row>\n");
}
/*******************************************************************************
**
*******************************************************************************/
public void createCell(int columnIndex, String value, int styleIndex) throws IOException
{
String ref = new CellReference(rowNo, columnIndex).formatAsString();
writer.write("<c r=\"" + ref + "\" t=\"inlineStr\"");
if(styleIndex != -1)
{
writer.write(" s=\"" + styleIndex + "\"");
}
String cleanValue = cleanseValue(value);
writer.write(">");
writer.write("<is><t>" + cleanValue + "</t></is>");
writer.write("</c>");
}
/*******************************************************************************
**
*******************************************************************************/
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("&amp;");
}
else if(c == '<')
{
rs.append("&lt;");
}
else if(c == '>')
{
rs.append("&gt;");
}
else if(c == '\'')
{
rs.append("&apos;");
}
else if(c == '"')
{
rs.append("&quot;");
}
else if (c < 32 && c != '\t' && c != '\n')
{
rs.append(' ');
}
else
{
rs.append(c);
}
}
Map<String, Integer> 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("<c r=\"" + ref + "\" t=\"n\"");
if(styleIndex != -1)
{
writer.write(" s=\"" + styleIndex + "\"");
}
writer.write(">");
writer.write("<v>" + value + "</v>");
writer.write("</c>");
}
/*******************************************************************************
**
*******************************************************************************/
public void createCell(int columnIndex, double value) throws IOException
{
createCell(columnIndex, value, -1);
}
/*******************************************************************************
**
*******************************************************************************/
public void createCell(int columnIndex, Boolean value) throws IOException
{
createCell(columnIndex, value, -1);
}
/*******************************************************************************
**
*******************************************************************************/
public void createCell(int columnIndex, Boolean value, int styleIndex) throws IOException
{
String ref = new CellReference(rowNo, columnIndex).formatAsString();
writer.write("<c r=\"" + ref + "\" t=\"b\"");
if(styleIndex != -1)
{
writer.write(" s=\"" + styleIndex + "\"");
}
writer.write(">");
if(value != null)
{
writer.write("<v>" + (value ? 1 : 0) + "</v>");
}
writer.write("</c>");
}
}

View File

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

View File

@ -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<HelpRole> roles, QHelpContent helpContent)
{
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
if(!StringUtils.hasContent(slotName))
@ -265,22 +264,14 @@ public class QInstanceHelpContentManager
}
else
{
Map<String, QHelpContent> 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);
}
}

View File

@ -1383,12 +1383,17 @@ public class QInstanceValidator
///////////////////////////////////
// validate steps in the process //
///////////////////////////////////
Set<String> 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++;
////////////////////////////////////////////

View File

@ -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<String> 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);
}
}

View File

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

View File

@ -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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>
{
XLSX,
CSV,
JSON;
public static final String NAME = "reportFormat";
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueId()
{
return name();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueLabel()
{
return name();
}
}

View File

@ -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<String, Serializable> 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);
}
}

View File

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

View File

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

View File

@ -0,0 +1,65 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.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;
}
}

View File

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

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -19,13 +19,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
package com.kingsrook.qqq.backend.core.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...)
}

View File

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

View File

@ -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);
}

View File

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

View File

@ -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;

View File

@ -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 //
//////////////////////////////////////////////////////////////////////////////////////////

View File

@ -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<String, QIcon> icons;
protected Map<String, QHelpContent> helpContent;
protected Map<String, List<QHelpContent>> helpContent;
protected Map<String, Serializable> defaultValues = new LinkedHashMap<>();
@ -691,10 +695,11 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
}
/*******************************************************************************
** Getter for helpContent
*******************************************************************************/
public Map<String, QHelpContent> getHelpContent()
public Map<String, List<QHelpContent>> getHelpContent()
{
return (this.helpContent);
}
@ -704,7 +709,7 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
/*******************************************************************************
** Setter for helpContent
*******************************************************************************/
public void setHelpContent(Map<String, QHelpContent> helpContent)
public void setHelpContent(Map<String, List<QHelpContent>> helpContent)
{
this.helpContent = helpContent;
}
@ -714,11 +719,49 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
/*******************************************************************************
** Fluent setter for helpContent
*******************************************************************************/
public QWidgetMetaData withHelpContent(Map<String, QHelpContent> helpContent)
public QWidgetMetaData withHelpContent(Map<String, List<QHelpContent>> 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<QHelpContent> 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<HelpRole> roles)
{
if(this.helpContent == null)
{
return;
}
List<QHelpContent> listForSlot = this.helpContent.get(slot);
if(listForSlot == null)
{
return;
}
QInstanceHelpContentManager.removeHelpContentByRoleSetFromList(roles, listForSlot);
}
}

View File

@ -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<String, QHelpContent> getHelpContent()
default Map<String, List<QHelpContent>> getHelpContent()
{
return (null);
}
@ -244,11 +246,29 @@ public interface QWidgetMetaDataInterface extends MetaDataWithPermissionRules, T
/*******************************************************************************
**
*******************************************************************************/
default void setHelpContent(Map<String, QHelpContent> helpContent)
default void setHelpContent(Map<String, List<QHelpContent>> 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<HelpRole> roles)
{
LOG.debug("Setting help content in a widgetMetaData type that doesn't support it (because it didn't override the getter/setter)");
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -27,11 +27,14 @@ import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -44,6 +47,7 @@ public enum DynamicDefaultValueBehavior implements FieldBehavior<DynamicDefaultV
{
CREATE_DATE,
MODIFY_DATE,
USER_ID,
NONE;
private static final QLogger LOG = QLogger.getLogger(ValueTooLongBehavior.class);
@ -76,6 +80,7 @@ public enum DynamicDefaultValueBehavior implements FieldBehavior<DynamicDefaultV
{
case CREATE_DATE -> 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<DynamicDefaultV
/*******************************************************************************
**
*******************************************************************************/
private void applyUserId(ValueBehaviorApplier.Action action, List<QRecord> 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);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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());

View File

@ -59,9 +59,9 @@ public class QFrontendWidgetMetaData
private boolean showReloadButton = false;
private boolean showExportButton = false;
protected Map<String, QIcon> icons;
protected Map<String, QHelpContent> helpContent;
protected Map<String, Serializable> defaultValues;
protected Map<String, QIcon> icons;
protected Map<String, List<QHelpContent>> helpContent;
protected Map<String, Serializable> defaultValues;
private final boolean hasPermission;
@ -273,7 +273,7 @@ public class QFrontendWidgetMetaData
** Getter for helpContent
**
*******************************************************************************/
public Map<String, QHelpContent> getHelpContent()
public Map<String, List<QHelpContent>> getHelpContent()
{
return helpContent;
}

View File

@ -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<String> titleFields;
private List<String> pivotFields;
private List<String> 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<QReportField> columns;
private List<QFilterOrderBy> 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<String> getPivotFields()
public List<String> getSummaryFields()
{
return pivotFields;
return summaryFields;
}
/*******************************************************************************
** Setter for pivotFields
** Setter for summaryFields
**
*******************************************************************************/
public void setPivotFields(List<String> pivotFields)
public void setSummaryFields(List<String> summaryFields)
{
this.pivotFields = pivotFields;
this.summaryFields = summaryFields;
}
/*******************************************************************************
** Fluent setter for pivotFields
** Fluent setter for summaryFields
**
*******************************************************************************/
public QReportView withPivotFields(List<String> pivotFields)
public QReportView withSummaryFields(List<String> 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);
}
}

View File

@ -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.
}

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.savedreports;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
/*******************************************************************************
**
*******************************************************************************/
public enum RenderedReportStatus implements PossibleValueEnum<Integer>
{
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;
}
}

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ReportColumn> columns;
/*******************************************************************************
** Getter for columns
*******************************************************************************/
public List<ReportColumn> getColumns()
{
return (this.columns);
}
/*******************************************************************************
**
*******************************************************************************/
public List<ReportColumn> 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<ReportColumn> columns)
{
this.columns = columns;
}
/*******************************************************************************
** Fluent setter for columns
*******************************************************************************/
public ReportColumns withColumns(List<ReportColumn> 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);
}
}

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
{
return (preInsertOrUpdate(records));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
{
return (preInsertOrUpdate(records));
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> preInsertOrUpdate(List<QRecord> 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<String> 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);
}
}
}

View File

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

View File

@ -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)

View File

@ -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;
@ -129,6 +130,16 @@ public interface QBackendModuleInterface
return null;
}
/*******************************************************************************
**
*******************************************************************************/
default QStorageInterface getStorageInterface()
{
throwNotImplemented("StorageInterface");
return null;
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -26,6 +26,7 @@ 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.QBackendModuleInterface;
@ -117,4 +118,14 @@ public class MemoryBackendModule implements QBackendModuleInterface
return (new MemoryDeleteAction());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QStorageInterface getStorageInterface()
{
return (new MemoryStorageAction());
}
}

View File

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

View File

@ -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<String, Serializable> values = runBackendStepInput.getValues();
reportInput.setInputValues(values);

View File

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

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ObjectMapper> 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<QReportField> reportColumns = new ArrayList<>();
view.setColumns(reportColumns);
Set<String> 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<QueryJoin> 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<String> summaryFields = new ArrayList<>();
List<QFilterOrderBy> 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<QReportField> 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<String> 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) {}
}

View File

@ -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 ")

View File

@ -701,7 +701,7 @@ public class QuartzScheduler implements QSchedulerInterface
/*******************************************************************************
**
*******************************************************************************/
List<QuartzJobAndTriggerWrapper> queryQuartz() throws SchedulerException
public List<QuartzJobAndTriggerWrapper> queryQuartz() throws SchedulerException
{
return queryQuartzMemoization.getResultThrowing(AnyKey.getInstance(), (x) ->
{

View File

@ -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));
}
}

View File

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

View File

@ -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<Boolean, ? extends Throwable> supplier)
{
try
{
return supplier.get();
}
catch(Throwable t)
{
return (false);
}
}
}

View File

@ -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<T extends Serializable>
public interface AggregatesInterface<T extends Serializable, AVG_T extends Serializable>
{
/*******************************************************************************
**
@ -60,5 +64,51 @@ public interface AggregatesInterface<T extends Serializable>
/*******************************************************************************
**
*******************************************************************************/
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);
}
}

View File

@ -28,13 +28,16 @@ import java.math.BigDecimal;
/*******************************************************************************
** BigDecimal version of data aggregator
*******************************************************************************/
public class BigDecimalAggregates implements AggregatesInterface<BigDecimal>
public class BigDecimalAggregates implements AggregatesInterface<BigDecimal, BigDecimal>
{
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<BigDecimal>
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<BigDecimal>
{
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<BigDecimal>
/*******************************************************************************
** Getter for product
**
*******************************************************************************/
@Override
public BigDecimal getProduct()
{
return product;
}
/*******************************************************************************
**
*******************************************************************************/

View File

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

View File

@ -28,13 +28,16 @@ import java.math.BigDecimal;
/*******************************************************************************
** Integer version of data aggregator
*******************************************************************************/
public class IntegerAggregates implements AggregatesInterface<Integer>
public class IntegerAggregates implements AggregatesInterface<Integer, BigDecimal>
{
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<Integer>
return;
}
BigDecimal inputBD = new BigDecimal(input);
count++;
if(sum == null)
@ -59,6 +64,15 @@ public class IntegerAggregates implements AggregatesInterface<Integer>
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<Integer>
{
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<Integer>
/*******************************************************************************
** Getter for product
**
*******************************************************************************/
@Override
public BigDecimal getProduct()
{
return product;
}
/*******************************************************************************
**
*******************************************************************************/

View File

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

View File

@ -28,13 +28,16 @@ import java.math.BigDecimal;
/*******************************************************************************
** Long version of data aggregator
*******************************************************************************/
public class LongAggregates implements AggregatesInterface<Long>
public class LongAggregates implements AggregatesInterface<Long, BigDecimal>
{
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<Long>
return;
}
BigDecimal inputBD = new BigDecimal(input);
count++;
if(sum == null)
@ -59,6 +64,15 @@ public class LongAggregates implements AggregatesInterface<Long>
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<Long>
{
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 LongAggregates implements AggregatesInterface<Long>
/*******************************************************************************
** Getter for product
**
*******************************************************************************/
@Override
public BigDecimal getProduct()
{
return product;
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,121 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.utils.aggregates;
/*******************************************************************************
** String version of data aggregator
*******************************************************************************/
public class StringAggregates implements AggregatesInterface<String, String>
{
private int count = 0;
private String min;
private String max;
/*******************************************************************************
** Add a new value to this aggregate set
*******************************************************************************/
public void add(String input)
{
if(input == null)
{
return;
}
count++;
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 String getSum()
{
//////////////////////////////////////
// sum of string doesn't make sense //
//////////////////////////////////////
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getMin()
{
return (min);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getMax()
{
return (max);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getAverage()
{
///////////////////////////////////////
// average string doesn't make sense //
///////////////////////////////////////
return (null);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.utils.aggregates;
import java.math.BigDecimal;
import java.math.RoundingMode;
/*******************************************************************************
** see https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm
**
*******************************************************************************/
public class VarianceCalculator
{
private int n;
private BigDecimal runningMean = BigDecimal.ZERO;
private BigDecimal m2 = BigDecimal.ZERO;
public static int scaleForVarianceCalculations = 4;
/*******************************************************************************
**
*******************************************************************************/
public void updateVariance(BigDecimal newInput)
{
n++;
BigDecimal delta = newInput.subtract(runningMean);
runningMean = runningMean.add(delta.divide(new BigDecimal(n), scaleForVarianceCalculations, RoundingMode.HALF_UP));
BigDecimal delta2 = newInput.subtract(runningMean);
m2 = m2.add(delta.multiply(delta2));
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getVariance()
{
if(n < 2)
{
return (null);
}
return m2.divide(new BigDecimal(n - 1), scaleForVarianceCalculations, RoundingMode.HALF_UP);
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getVarP()
{
if(n < 2)
{
return (null);
}
return m2.divide(new BigDecimal(n), scaleForVarianceCalculations, RoundingMode.HALF_UP);
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getStandardDeviation()
{
BigDecimal variance = getVariance();
if(variance == null)
{
return (null);
}
return BigDecimal.valueOf(Math.sqrt(variance.doubleValue())).setScale(scaleForVarianceCalculations, RoundingMode.HALF_UP);
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getStdDevP()
{
BigDecimal varP = getVarP();
if(varP == null)
{
return (null);
}
return BigDecimal.valueOf(Math.sqrt(varP.doubleValue())).setScale(scaleForVarianceCalculations, RoundingMode.HALF_UP);
}
}

View File

@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -43,6 +44,7 @@ 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.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.LocalMacDevUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
@ -118,6 +120,26 @@ class ExportActionTest extends BaseTest
runReport(recordCount, filename, ReportFormat.XLSX, true);
File file = new File(filename);
LocalMacDevUtils.openFile(file.getAbsolutePath());
assertTrue(file.delete());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
public void testExcelPOI() throws Exception
{
int recordCount = 1000;
String filename = "/tmp/ReportActionTest-POI.xlsx";
runReport(recordCount, filename, ReportFormat.XLSX, true);
File file = new File(filename);
LocalMacDevUtils.openFile(file.getAbsolutePath());
assertTrue(file.delete());
}
@ -147,9 +169,10 @@ class ExportActionTest extends BaseTest
ExportInput exportInput = new ExportInput();
exportInput.setTableName(TestUtils.TABLE_NAME_ORDER);
exportInput.setReportFormat(ReportFormat.CSV);
ByteArrayOutputStream reportOutputStream = new ByteArrayOutputStream();
exportInput.setReportOutputStream(reportOutputStream);
exportInput.setReportDestination(new ReportDestination()
.withReportFormat(ReportFormat.CSV)
.withReportOutputStream(reportOutputStream));
exportInput.setQueryFilter(new QQueryFilter());
exportInput.setFieldNames(List.of("id", "orderNo", "storeId", "orderLine.id", "orderLine.sku", "orderLine.quantity"));
// exportInput.setFieldNames(List.of("id", "orderNo", "storeId"));
@ -197,8 +220,7 @@ class ExportActionTest extends BaseTest
exportInput.setTableName("person");
QTableMetaData table = exportInput.getTable();
exportInput.setReportFormat(reportFormat);
exportInput.setReportOutputStream(outputStream);
exportInput.setReportDestination(new ReportDestination().withReportFormat(reportFormat).withReportOutputStream(outputStream));
exportInput.setQueryFilter(new QQueryFilter());
exportInput.setLimit(recordCount);
@ -243,7 +265,7 @@ class ExportActionTest extends BaseTest
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// use xlsx, which has a max-rows limit, to verify that code runs, but doesn't throw when there aren't too many rows //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
exportInput.setReportFormat(ReportFormat.XLSX);
exportInput.setReportDestination(new ReportDestination().withReportFormat(ReportFormat.XLSX));
new ExportAction().preExecute(exportInput);
@ -278,7 +300,7 @@ class ExportActionTest extends BaseTest
////////////////////////////////////////////////////////////////
// use xlsx, which has a max-cols limit, to verify that code. //
////////////////////////////////////////////////////////////////
exportInput.setReportFormat(ReportFormat.XLSX);
exportInput.setReportDestination(new ReportDestination().withReportFormat(ReportFormat.XLSX));
assertThrows(QUserFacingException.class, () ->
{

View File

@ -23,19 +23,33 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.Month;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel.ExcelFastexcelExportStreamer;
import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.BoldHeaderAndFooterPoiExcelStyler;
import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.ExcelPoiBasedStreamingExportStreamer;
import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.PoiExcelStylerInterface;
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.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.actions.reporting.pivottable.PivotTableDefinition;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableFunction;
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.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
@ -51,7 +65,17 @@ 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.modules.backend.implementations.memory.MemoryRecordStore;
import com.kingsrook.qqq.backend.core.testutils.PersonQRecord;
import com.kingsrook.qqq.backend.core.utils.LocalMacDevUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFPivotTable;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -85,14 +109,14 @@ public class GenerateReportActionTest extends BaseTest
**
*******************************************************************************/
@Test
void testPivot1() throws QException
void testSummary1() throws QException
{
QInstance qInstance = QContext.getQInstance();
qInstance.addReport(definePersonShoesPivotReport(true));
qInstance.addReport(definePersonShoesSummaryReport(true));
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1), "endDate", LocalDate.of(1980, Month.DECEMBER, 31)));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("summary");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(3, list.size());
@ -140,10 +164,10 @@ public class GenerateReportActionTest extends BaseTest
**
*******************************************************************************/
@Test
void testPivot2() throws QException
void testSummary2() throws QException
{
QInstance qInstance = QContext.getQInstance();
QReportMetaData report = definePersonShoesPivotReport(false);
QReportMetaData report = definePersonShoesSummaryReport(false);
//////////////////////////////////////////////
// change from the default to sort reversed //
@ -153,7 +177,7 @@ public class GenerateReportActionTest extends BaseTest
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1), "endDate", LocalDate.of(1980, Month.DECEMBER, 31)));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("summary");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(2, list.size());
@ -172,10 +196,10 @@ public class GenerateReportActionTest extends BaseTest
**
*******************************************************************************/
@Test
void testPivot3() throws QException
void testSummary3() throws QException
{
QInstance qInstance = QContext.getQInstance();
QReportMetaData report = definePersonShoesPivotReport(false);
QReportMetaData report = definePersonShoesSummaryReport(false);
//////////////////////////////////////////////////////////////////////////////////////////////
// remove the filters, change to sort by personCount (to get some ties), then sumPrice desc //
@ -187,7 +211,7 @@ public class GenerateReportActionTest extends BaseTest
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.now(), "endDate", LocalDate.now()));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("summary");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
@ -224,16 +248,16 @@ public class GenerateReportActionTest extends BaseTest
**
*******************************************************************************/
@Test
void testPivot4() throws QException
void testSummary4() throws QException
{
QInstance qInstance = QContext.getQInstance();
QReportMetaData report = definePersonShoesPivotReport(false);
QReportMetaData report = definePersonShoesSummaryReport(false);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// remove the filter, change to have 2 pivot columns - homeStateId and lastName - we should get no roll-up like this. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
report.getDataSources().get(0).getQueryFilter().setCriteria(null);
report.getViews().get(0).setPivotFields(List.of(
report.getViews().get(0).setSummaryFields(List.of(
"homeStateId",
"lastName"
));
@ -241,7 +265,7 @@ public class GenerateReportActionTest extends BaseTest
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.now(), "endDate", LocalDate.now()));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("summary");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(6, list.size());
@ -282,21 +306,21 @@ public class GenerateReportActionTest extends BaseTest
**
*******************************************************************************/
@Test
void testPivot5() throws QException
void testSummary5() throws QException
{
QInstance qInstance = QContext.getQInstance();
QReportMetaData report = definePersonShoesPivotReport(false);
QReportMetaData report = definePersonShoesSummaryReport(false);
/////////////////////////////////////////////////////////////////////////////////////
// remove the filter, and just pivot on homeStateId - should aggregate differently //
/////////////////////////////////////////////////////////////////////////////////////
report.getDataSources().get(0).getQueryFilter().setCriteria(null);
report.getViews().get(0).setPivotFields(List.of("homeStateId"));
report.getViews().get(0).setSummaryFields(List.of("homeStateId"));
qInstance.addReport(report);
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.now(), "endDate", LocalDate.now()));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("pivot");
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("summary");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(2, list.size());
@ -315,23 +339,18 @@ public class GenerateReportActionTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void runToCsv() throws Exception
private String runToString(ReportFormat reportFormat, String reportName) throws Exception
{
String name = "/tmp/report.csv";
String name = "/tmp/report." + reportFormat.getExtension();
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
{
QInstance qInstance = QContext.getQInstance();
qInstance.addReport(definePersonShoesPivotReport(true));
insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput();
reportInput.setReportName(REPORT_NAME);
reportInput.setReportFormat(ReportFormat.CSV);
reportInput.setReportOutputStream(fileOutputStream);
reportInput.setReportName(reportName);
reportInput.setReportDestination(new ReportDestination().withReportFormat(reportFormat).withReportOutputStream(fileOutputStream));
reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
new GenerateReportAction().execute(reportInput);
System.out.println("Wrote File: " + name);
return (FileUtils.readFileToString(new File(name), StandardCharsets.UTF_8));
}
}
@ -341,27 +360,189 @@ public class GenerateReportActionTest extends BaseTest
**
*******************************************************************************/
@Test
void runToXlsx() throws Exception
void runSummaryToXlsx() throws Exception
{
String name = "/tmp/report.xlsx";
ReportFormat format = ReportFormat.XLSX;
String name = "/tmp/report-" + format + ".xlsx";
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
{
QInstance qInstance = QContext.getQInstance();
qInstance.addReport(definePersonShoesPivotReport(true));
qInstance.addReport(definePersonShoesSummaryReport(true));
insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput();
reportInput.setReportName(REPORT_NAME);
reportInput.setReportFormat(ReportFormat.XLSX);
reportInput.setReportOutputStream(fileOutputStream);
reportInput.setReportDestination(new ReportDestination().withReportFormat(format).withReportOutputStream(fileOutputStream));
reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
new GenerateReportAction().execute(reportInput);
System.out.println("Wrote File: " + name);
LocalMacDevUtils.openFile(name);
}
}
/*******************************************************************************
** Keep some test coverage on the fastexcel library (as long as we keep it around)
*******************************************************************************/
@Test
void runTableToXlsxFastexcel() throws Exception
{
ReportFormat format = ReportFormat.XLSX;
String name = "/tmp/report-fastexcel.xlsx";
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
{
QInstance qInstance = QContext.getQInstance();
qInstance.addReport(defineTableOnlyReport());
insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput();
reportInput.setReportName(REPORT_NAME);
reportInput.setReportDestination(new ReportDestination().withReportFormat(format).withReportOutputStream(fileOutputStream));
reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
reportInput.setOverrideExportStreamerSupplier(ExcelFastexcelExportStreamer::new);
new GenerateReportAction().execute(reportInput);
System.out.println("Wrote File: " + name);
LocalMacDevUtils.openFile(name);
}
}
/*******************************************************************************
** Imagine if we used the boldHeaderAndFooter styler
*******************************************************************************/
@Test
void runTableToXlsxWithOverrideStyles() throws Exception
{
ReportFormat format = ReportFormat.XLSX;
String name = "/tmp/report-fastexcel.xlsx";
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
{
QInstance qInstance = QContext.getQInstance();
QReportMetaData reportMetaData = defineTableOnlyReport();
reportMetaData.getViews().get(0).withTitleFormat("My Title");
qInstance.addReport(reportMetaData);
insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput();
reportInput.setReportName(REPORT_NAME);
reportInput.setReportDestination(new ReportDestination().withReportFormat(format).withReportOutputStream(fileOutputStream));
reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
reportInput.setOverrideExportStreamerSupplier(() -> new ExcelPoiBasedStreamingExportStreamer()
{
@Override
protected PoiExcelStylerInterface getStylerInterface()
{
return new BoldHeaderAndFooterPoiExcelStyler();
}
});
new GenerateReportAction().execute(reportInput);
System.out.println("Wrote File: " + name);
LocalMacDevUtils.openFile(name);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void runTableToXlsx() throws Exception
{
ReportFormat format = ReportFormat.XLSX;
String name = "/tmp/report-" + format + ".xlsx";
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
{
QInstance qInstance = QContext.getQInstance();
qInstance.addReport(defineTableOnlyReport());
insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput();
reportInput.setReportName(REPORT_NAME);
reportInput.setReportDestination(new ReportDestination().withReportFormat(format).withReportOutputStream(fileOutputStream));
reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
new GenerateReportAction().execute(reportInput);
System.out.println("Wrote File: " + name);
LocalMacDevUtils.openFile(name);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void runPivotToXlsx() throws Exception
{
String name = "/tmp/pivot-test.xlsx";
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
{
QInstance qInstance = QContext.getQInstance();
qInstance.addReport(definePivotReport());
insertPersonRecords(qInstance);
ReportInput reportInput = new ReportInput();
reportInput.setReportName(REPORT_NAME);
reportInput.setReportDestination(new ReportDestination().withReportFormat(ReportFormat.XLSX).withReportOutputStream(fileOutputStream));
reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
new GenerateReportAction().execute(reportInput);
System.out.println("Wrote File: " + name);
LocalMacDevUtils.openFile(name, "/Applications/Numbers.app");
}
///////////////////////////////////////////////////////////
// read the file we wrote, and assert about its contents //
///////////////////////////////////////////////////////////
FileInputStream file = new FileInputStream(name);
XSSFWorkbook workbook = new XSSFWorkbook(file);
XSSFSheet sheet = workbook.getSheetAt(1);
List<XSSFPivotTable> pivotTables = sheet.getPivotTables();
XSSFPivotTable xssfPivotTable = pivotTables.get(0);
List<Integer> rowLabelColumns = xssfPivotTable.getRowLabelColumns();
List<Integer> colLabelColumns = xssfPivotTable.getColLabelColumns();
Sheet dataSheet = xssfPivotTable.getDataSheet();
Sheet parentSheet = xssfPivotTable.getParentSheet();
System.out.println();
Map<Integer, List<Object>> data = new HashMap<>();
int i = 0;
for(Row row : sheet)
{
data.put(i, new ArrayList<>());
for(Cell cell : row)
{
data.get(i).add(switch(cell.getCellType())
{
case _NONE -> "<_NONE>";
case NUMERIC -> cell.getNumericCellValue();
case STRING -> cell.getStringCellValue();
case FORMULA -> cell.getCellFormula();
case BLANK -> "<BLANK>";
case BOOLEAN -> cell.getBooleanCellValue();
case ERROR -> cell.getErrorCellValue();
});
}
i++;
}
System.out.println(data);
}
/*******************************************************************************
**
*******************************************************************************/
@ -369,8 +550,7 @@ public class GenerateReportActionTest extends BaseTest
{
ReportInput reportInput = new ReportInput();
reportInput.setReportName(REPORT_NAME);
reportInput.setReportFormat(ReportFormat.LIST_OF_MAPS);
reportInput.setReportOutputStream(new ByteArrayOutputStream());
reportInput.setReportDestination(new ReportDestination().withReportFormat(ReportFormat.LIST_OF_MAPS).withReportOutputStream(new ByteArrayOutputStream()));
reportInput.setInputValues(inputValues);
new GenerateReportAction().execute(reportInput);
}
@ -380,15 +560,15 @@ public class GenerateReportActionTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
private void insertPersonRecords(QInstance qInstance) throws QException
public static void insertPersonRecords(QInstance qInstance) throws QException
{
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
new PersonQRecord().withLastName("Jonson").withBirthDate(LocalDate.of(1980, Month.JANUARY, 31)).withNoOfShoes(null).withHomeStateId(1).withPrice(null).withCost(new BigDecimal("0.50")), // wrong last initial
new PersonQRecord().withLastName("Jones").withBirthDate(LocalDate.of(1980, Month.JANUARY, 31)).withNoOfShoes(3).withHomeStateId(1).withPrice(new BigDecimal("1.00")).withCost(new BigDecimal("0.50")), // wrong last initial
new PersonQRecord().withLastName("Kelly").withBirthDate(LocalDate.of(1979, Month.DECEMBER, 30)).withNoOfShoes(4).withHomeStateId(1).withPrice(new BigDecimal("1.20")).withCost(new BigDecimal("0.50")), // bad birthdate
new PersonQRecord().withLastName("Keller").withBirthDate(LocalDate.of(1980, Month.JANUARY, 7)).withNoOfShoes(5).withHomeStateId(1).withPrice(new BigDecimal("2.40")).withCost(new BigDecimal("3.50")),
new PersonQRecord().withLastName("Kelkhoff").withBirthDate(LocalDate.of(1980, Month.FEBRUARY, 15)).withNoOfShoes(6).withHomeStateId(1).withPrice(new BigDecimal("3.60")).withCost(new BigDecimal("3.50")),
new PersonQRecord().withLastName("Kelkhoff").withBirthDate(LocalDate.of(1980, Month.MARCH, 20)).withNoOfShoes(7).withHomeStateId(2).withPrice(new BigDecimal("4.80")).withCost(new BigDecimal("3.50"))
new PersonQRecord().withFirstName("Darin").withLastName("Jonson").withBirthDate(LocalDate.of(1980, Month.JANUARY, 31)).withNoOfShoes(null).withHomeStateId(1).withPrice(null).withCost(new BigDecimal("0.50")), // wrong last initial
new PersonQRecord().withFirstName("Darin").withLastName("Jones").withBirthDate(LocalDate.of(1980, Month.JANUARY, 31)).withNoOfShoes(3).withHomeStateId(1).withPrice(new BigDecimal("1.00")).withCost(new BigDecimal("0.50")), // wrong last initial
new PersonQRecord().withFirstName("Darin").withLastName("Kelly").withBirthDate(LocalDate.of(1979, Month.DECEMBER, 30)).withNoOfShoes(4).withHomeStateId(1).withPrice(new BigDecimal("1.20")).withCost(new BigDecimal("0.50")), // bad birthdate
new PersonQRecord().withFirstName("Trevor").withLastName("Keller").withBirthDate(LocalDate.of(1980, Month.JANUARY, 7)).withNoOfShoes(5).withHomeStateId(1).withPrice(new BigDecimal("2.40")).withCost(new BigDecimal("3.50")),
new PersonQRecord().withFirstName("Trevor").withLastName("Kelkhoff").withBirthDate(LocalDate.of(1980, Month.FEBRUARY, 15)).withNoOfShoes(6).withHomeStateId(1).withPrice(new BigDecimal("3.60")).withCost(new BigDecimal("3.50")),
new PersonQRecord().withFirstName("Kelly").withLastName("Kelkhoff").withBirthDate(LocalDate.of(1980, Month.MARCH, 20)).withNoOfShoes(7).withHomeStateId(2).withPrice(new BigDecimal("4.80")).withCost(new BigDecimal("3.50"))
));
}
@ -397,7 +577,7 @@ public class GenerateReportActionTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
public static QReportMetaData definePersonShoesPivotReport(boolean includeTotalRow)
public static QReportMetaData definePersonShoesSummaryReport(boolean includeTotalRow)
{
return new QReportMetaData()
.withName(REPORT_NAME)
@ -416,11 +596,11 @@ public class GenerateReportActionTest extends BaseTest
))
.withViews(List.of(
new QReportView()
.withName("pivot")
.withLabel("pivot")
.withName("summary")
.withLabel("summary")
.withDataSourceName("persons")
.withType(ReportType.SUMMARY)
.withPivotFields(List.of("lastName"))
.withSummaryFields(List.of("lastName"))
.withIncludeTotalRow(includeTotalRow)
.withTitleFormat("Number of shoes - people born between %s and %s - pivot on LastName, sort by Quantity, Revenue DESC")
.withTitleFields(List.of("${input.startDate}", "${input.endDate}"))
@ -452,39 +632,14 @@ public class GenerateReportActionTest extends BaseTest
@Test
void testTableOnlyReport() throws QException
{
QInstance qInstance = QContext.getQInstance();
QReportMetaData report = new QReportMetaData()
.withName(REPORT_NAME)
.withDataSources(List.of(
new QReportDataSource()
.withName("persons")
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, List.of("${input.startDate}")))
)
))
.withInputFields(List.of(
new QFieldMetaData("startDate", QFieldType.DATE_TIME)
))
.withViews(List.of(
new QReportView()
.withName("table1")
.withLabel("table1")
.withDataSourceName("persons")
.withType(ReportType.TABLE)
.withColumns(List.of(
new QReportField().withName("id"),
new QReportField().withName("firstName"),
new QReportField().withName("lastName")
))
));
QInstance qInstance = QContext.getQInstance();
QReportMetaData report = defineTableOnlyReport();
qInstance.addReport(report);
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1)));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("table1");
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("Table 1");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(5, list.size());
@ -493,6 +648,88 @@ public class GenerateReportActionTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
private static QReportMetaData defineTableOnlyReport()
{
QReportMetaData report = new QReportMetaData()
.withName(REPORT_NAME)
.withDataSources(List.of(
new QReportDataSource()
.withName("persons")
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, List.of("${input.startDate}"))))))
.withInputFields(List.of(
new QFieldMetaData("startDate", QFieldType.DATE_TIME)))
.withViews(List.of(
new QReportView()
.withName("table1")
.withLabel("Table 1")
.withDataSourceName("persons")
.withType(ReportType.TABLE)
.withColumns(List.of(
new QReportField().withName("id"),
new QReportField().withName("firstName"),
new QReportField().withName("lastName")))));
return report;
}
/*******************************************************************************
**
*******************************************************************************/
private static QReportMetaData definePivotReport()
{
QReportMetaData report = new QReportMetaData()
.withName(REPORT_NAME)
.withDataSources(List.of(
new QReportDataSource()
.withName("persons")
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.GREATER_THAN, List.of("${input.startDate}"))))))
.withInputFields(List.of(
new QFieldMetaData("startDate", QFieldType.DATE_TIME)))
.withViews(List.of(
new QReportView()
.withName("table1")
.withLabel("Table 1")
.withDataSourceName("persons")
.withType(ReportType.TABLE)
.withColumns(List.of(
new QReportField().withName("id"),
new QReportField().withName("firstName"),
new QReportField().withName("lastName"),
new QReportField().withName("homeStateId")
)),
new QReportView()
.withName("pivotTable1")
.withLabel("My Pivot Table")
.withType(ReportType.PIVOT)
.withPivotTableSourceViewName("table1")
.withPivotTableDefinition(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("homeStateId"))
// .withRow(new PivotTableGroupBy().withFieldName("lastName"))
// .withColumn(new PivotTableGroupBy().withFieldName("firstName"))
.withValue(new PivotTableValue().withFunction(PivotTableFunction.COUNT).withFieldName("id")))
));
return report;
}
/*******************************************************************************
**
*******************************************************************************/
@ -500,6 +737,180 @@ public class GenerateReportActionTest extends BaseTest
void testTwoTableViewsOneDataSourceReport() throws QException
{
QInstance qInstance = QContext.getQInstance();
defineTwoViewsOneDataSourceReport(qInstance);
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1)));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("Table 1");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(5, list.size());
assertThat(row).containsOnlyKeys("Id", "First Name", "Last Name");
list = ListOfMapsExportStreamer.getList("Table 2");
iterator = list.iterator();
row = iterator.next();
assertEquals(5, list.size());
assertThat(row).containsOnlyKeys("Birth Date");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testOneTableViewsOneDataSourceJsonReport() throws Exception
{
QInstance qInstance = QContext.getQInstance();
QReportMetaData report = defineTableOnlyReport();
qInstance.addReport(report);
insertPersonRecords(qInstance);
String json = runToString(ReportFormat.JSON, report.getName());
// System.out.println(json);
/////////////////////////////////////////////////////////////////////////////////
// for a one-view report, we should just have an array of the report's records //
/////////////////////////////////////////////////////////////////////////////////
JSONArray jsonArray = new JSONArray(json);
assertEquals(6, jsonArray.length());
assertThat(jsonArray.getJSONObject(0).toMap())
.hasFieldOrPropertyWithValue("id", 1)
.hasFieldOrPropertyWithValue("firstName", "Darin")
.hasFieldOrPropertyWithValue("lastName", "Jonson");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTwoTableViewsOneDataSourceJsonReport() throws Exception
{
QInstance qInstance = QContext.getQInstance();
QReportMetaData report = defineTwoViewsOneDataSourceReport(qInstance);
insertPersonRecords(qInstance);
String json = runToString(ReportFormat.JSON, report.getName());
// System.out.println(json);
/////////////////////////////////////////////////////////////////////////////////
// for a multi-view report, we should have an array with the views as elements //
/////////////////////////////////////////////////////////////////////////////////
JSONArray jsonArray = new JSONArray(json);
assertEquals(2, jsonArray.length());
JSONObject firstView = jsonArray.getJSONObject(0);
assertEquals("Table 1", firstView.getString("name"));
JSONArray firstViewData = firstView.getJSONArray("data");
assertEquals(6, firstViewData.length());
assertThat(firstViewData.getJSONObject(0).toMap())
.hasFieldOrPropertyWithValue("id", 1)
.hasFieldOrPropertyWithValue("firstName", "Darin")
.hasFieldOrPropertyWithValue("lastName", "Jonson");
JSONObject secondView = jsonArray.getJSONObject(1);
assertEquals("Table 2", secondView.getString("name"));
JSONArray secondViewData = secondView.getJSONArray("data");
assertEquals(6, secondViewData.length());
assertThat(secondViewData.getJSONObject(0).toMap())
.hasFieldOrPropertyWithValue("birthDate", "1980-01-31");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableViewsAndSummaryViewJsonReport() throws Exception
{
QInstance qInstance = QContext.getQInstance();
QReportMetaData report = defineSimplePersonTableAndSummaryByFirstNameReport();
qInstance.addReport(report);
insertPersonRecords(qInstance);
String json = runToString(ReportFormat.JSON, report.getName());
System.out.println(json);
/////////////////////////////////////////////////////////////////////////////////
// for a multi-view report, we should have an array with the views as elements //
/////////////////////////////////////////////////////////////////////////////////
JSONArray jsonArray = new JSONArray(json);
assertEquals(2, jsonArray.length());
JSONObject firstView = jsonArray.getJSONObject(0);
assertEquals("Table 1", firstView.getString("name"));
JSONArray firstViewData = firstView.getJSONArray("data");
assertEquals(6, firstViewData.length());
assertThat(firstViewData.getJSONObject(0).toMap())
.hasFieldOrPropertyWithValue("id", 1)
.hasFieldOrPropertyWithValue("firstName", "Darin")
.hasFieldOrPropertyWithValue("lastName", "Jonson");
JSONObject secondView = jsonArray.getJSONObject(1);
assertEquals("Summary", secondView.getString("name"));
JSONArray secondViewData = secondView.getJSONArray("data");
assertEquals(4, secondViewData.length());
assertThat(secondViewData.getJSONObject(0).toMap())
.hasFieldOrPropertyWithValue("firstName", "Darin")
.hasFieldOrPropertyWithValue("personCount", 3);
assertThat(secondViewData.getJSONObject(3).toMap())
.hasFieldOrPropertyWithValue("firstName", "Totals")
.hasFieldOrPropertyWithValue("personCount", 6);
}
/*******************************************************************************
**
*******************************************************************************/
public static QReportMetaData defineSimplePersonTableAndSummaryByFirstNameReport()
{
return new QReportMetaData()
.withName(REPORT_NAME)
.withDataSources(List.of(
new QReportDataSource()
.withName("persons")
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilter(new QQueryFilter())
))
.withViews(List.of(
new QReportView()
.withName("table1")
.withLabel("Table 1")
.withDataSourceName("persons")
.withType(ReportType.TABLE)
.withColumns(List.of(
new QReportField().withName("id"),
new QReportField().withName("firstName"),
new QReportField().withName("lastName"),
new QReportField().withName("homeStateId")
)),
new QReportView()
.withName("summary")
.withLabel("Summary")
.withDataSourceName("persons")
.withType(ReportType.SUMMARY)
.withSummaryFields(List.of("firstName"))
.withIncludeTotalRow(true)
.withOrderByFields(List.of(new QFilterOrderBy("personCount", false)))
.withColumns(List.of(
new QReportField().withName("personCount").withLabel("Person Count").withFormula("${pivot.count.id}").withDisplayFormat(DisplayFormat.COMMAS)
))
));
}
/*******************************************************************************
**
*******************************************************************************/
private static QReportMetaData defineTwoViewsOneDataSourceReport(QInstance qInstance)
{
QReportMetaData report = new QReportMetaData()
.withName(REPORT_NAME)
.withDataSources(List.of(
@ -516,7 +927,7 @@ public class GenerateReportActionTest extends BaseTest
.withViews(List.of(
new QReportView()
.withName("table1")
.withLabel("table1")
.withLabel("Table 1")
.withDataSourceName("persons")
.withType(ReportType.TABLE)
.withColumns(List.of(
@ -526,7 +937,7 @@ public class GenerateReportActionTest extends BaseTest
)),
new QReportView()
.withName("table2")
.withLabel("table2")
.withLabel("Table 2")
.withDataSourceName("persons")
.withType(ReportType.TABLE)
.withColumns(List.of(
@ -535,21 +946,7 @@ public class GenerateReportActionTest extends BaseTest
));
qInstance.addReport(report);
insertPersonRecords(qInstance);
runReport(qInstance, Map.of("startDate", LocalDate.of(1980, Month.JANUARY, 1)));
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("table1");
Iterator<Map<String, String>> iterator = list.iterator();
Map<String, String> row = iterator.next();
assertEquals(5, list.size());
assertThat(row).containsOnlyKeys("Id", "First Name", "Last Name");
list = ListOfMapsExportStreamer.getList("table2");
iterator = list.iterator();
row = iterator.next();
assertEquals(5, list.size());
assertThat(row).containsOnlyKeys("Birth Date");
return (report);
}
@ -566,8 +963,7 @@ public class GenerateReportActionTest extends BaseTest
ReportInput reportInput = new ReportInput();
reportInput.setReportName(TestUtils.REPORT_NAME_PERSON_SIMPLE);
reportInput.setReportFormat(ReportFormat.LIST_OF_MAPS);
reportInput.setReportOutputStream(new ByteArrayOutputStream());
reportInput.setReportDestination(new ReportDestination().withReportFormat(ReportFormat.LIST_OF_MAPS).withReportOutputStream(new ByteArrayOutputStream()));
new GenerateReportAction().execute(reportInput);
List<Map<String, String>> list = ListOfMapsExportStreamer.getList("Simple Report");

View File

@ -0,0 +1,54 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for JsonExportStreamer
*******************************************************************************/
class JsonExportStreamerTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
Function<String, String> runOne = label -> new JsonExportStreamer().getLabelForJson(new QFieldMetaData("test", QFieldType.STRING).withLabel(label));
assertEquals("sku", runOne.apply("SKU"));
assertEquals("clientName", runOne.apply("Client Name"));
assertEquals("slaStatus", runOne.apply("SLA Status"));
assertEquals("lineItem:sku", runOne.apply("Line Item: SKU"));
assertEquals("parcel:slaStatus", runOne.apply("Parcel: SLA Status"));
assertEquals("order:client", runOne.apply("Order: Client"));
}
}

View File

@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateInpu
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
import com.kingsrook.qqq.backend.core.utils.LocalMacDevUtils;
import org.junit.jupiter.api.Test;
@ -107,8 +108,8 @@ class ConvertHtmlToPdfActionTest extends BaseTest
/////////////////////////////////////////////////////////////////////////
// for local dev on a mac, turn this on to auto-open the generated PDF //
/////////////////////////////////////////////////////////////////////////
// todo not commit
// Runtime.getRuntime().exec(new String[] { "/usr/bin/open", "/tmp/file.pdf" });
// LocalMacDevUtils.mayOpenFiles = true;
LocalMacDevUtils.openFile("/tmp/file.pdf");
}
}

View File

@ -216,7 +216,7 @@ class QInstanceHelpContentManagerTest extends BaseTest
// now - post-insert customizer should have automatically added help content to the instance //
///////////////////////////////////////////////////////////////////////////////////////////////
assertTrue(widget.getHelpContent().containsKey("label"));
assertEquals("i need somebody", widget.getHelpContent().get("label").getContent());
assertEquals("i need somebody", widget.getHelpContent().get("label").get(0).getContent());
}

View File

@ -272,6 +272,25 @@ public class QInstanceValidatorTest extends BaseTest
/*******************************************************************************
** Test rules for process step names (must be set; must not be duplicated)
**
*******************************************************************************/
@Test
public void test_validateProcessStepNames()
{
assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE).getStepList().get(0).setName(null),
"Missing name for a step at index");
assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE).getStepList().get(0).setName(""),
"Missing name for a step at index");
assertValidationFailureReasons((qInstance) -> qInstance.getProcess(TestUtils.PROCESS_NAME_GREET_PEOPLE_INTERACTIVE).getStepList().forEach(s -> s.setName("myStep")),
"Duplicate step name [myStep]", "Duplicate step name [myStep]");
}
/*******************************************************************************
** Test that a process with a step that is a private class fails
**

View File

@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -156,4 +157,47 @@ class DynamicDefaultValueBehaviorTest extends BaseTest
assertNull(record.getValue("firstName"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUserId()
{
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
table.getField("firstName").withBehavior(DynamicDefaultValueBehavior.USER_ID);
{
////////////////////////////////
// set it (if null) on insert //
////////////////////////////////
QRecord record = new QRecord().withValue("id", 1);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record), null);
assertEquals(QContext.getQSession().getUser().getIdReference(), record.getValue("firstName"));
}
{
////////////////////////////////
// set it (if null) on update //
////////////////////////////////
QRecord record = new QRecord().withValue("id", 1);
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, qInstance, table, List.of(record), null);
assertEquals(QContext.getQSession().getUser().getIdReference(), record.getValue("firstName"));
}
{
////////////////////////////////////////////////////////////////////
// only set it if it wasn't previously set (both insert & update) //
////////////////////////////////////////////////////////////////////
QRecord record = new QRecord().withValue("id", 1).withValue("firstName", "Bob");
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, List.of(record), null);
assertEquals("Bob", record.getValue("firstName"));
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, qInstance, table, List.of(record), null);
assertEquals("Bob", record.getValue("firstName"));
}
}
}

View File

@ -0,0 +1,166 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.savedreports;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
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.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.metadata.QInstance;
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.TestUtils;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for SavedReportJsonFieldDisplayValueFormatter
*******************************************************************************/
class SavedReportJsonFieldDisplayValueFormatterTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws QException
{
QContext.getQInstance().add(new SavedReportsMetaDataProvider().defineSavedReportTable(TestUtils.MEMORY_BACKEND_NAME, null));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPostQuery() throws QException
{
UnsafeFunction<SavedReport, QRecord, QException> customize = savedReport ->
{
QInstance qInstance = QContext.getQInstance();
QTableMetaData table = qInstance.getTable(SavedReport.TABLE_NAME);
QRecord record = savedReport.toQRecord();
for(String fieldName : List.of("queryFilterJson", "columnsJson", "pivotTableJson"))
{
SavedReportJsonFieldDisplayValueFormatter.getInstance().apply(ValueBehaviorApplier.Action.FORMATTING, List.of(record), qInstance, table, table.getField(fieldName));
}
return (record);
};
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withColumnsJson(JsonUtils.toJson(new ReportColumns()))
.withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition())));
assertEquals("0 Filters", record.getDisplayValue("queryFilterJson"));
assertEquals("0 Columns", record.getDisplayValue("columnsJson"));
assertEquals("0 Rows, 0 Columns, and 0 Values", record.getDisplayValue("pivotTableJson"));
}
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withColumnsJson(JsonUtils.toJson(new ReportColumns())));
assertEquals("0 Filters", record.getDisplayValue("queryFilterJson"));
assertEquals("0 Columns", record.getDisplayValue("columnsJson"));
assertNull(record.getDisplayValue("pivotTableJson"));
}
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()
.withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.IS_NOT_BLANK))))
.withColumnsJson(JsonUtils.toJson(new ReportColumns()
.withColumn(new ReportColumn().withName("birthDate"))))
.withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition()
.withRow(new PivotTableGroupBy())
.withValue(new PivotTableValue())
)));
assertEquals("1 Filter", record.getDisplayValue("queryFilterJson"));
assertEquals("1 Column", record.getDisplayValue("columnsJson"));
assertEquals("1 Row, 0 Columns, and 1 Value", record.getDisplayValue("pivotTableJson"));
}
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()
.withCriteria(new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN, 1))
.withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.IS_NOT_BLANK))))
.withColumnsJson(JsonUtils.toJson(new ReportColumns()
.withColumn(new ReportColumn().withName("__check__").withIsVisible(true))
.withColumn(new ReportColumn().withName("id"))
.withColumn(new ReportColumn().withName("firstName").withIsVisible(true))
.withColumn(new ReportColumn().withName("lastName").withIsVisible(false))
.withColumn(new ReportColumn().withName("birthDate"))))
.withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition()
.withRow(new PivotTableGroupBy())
.withRow(new PivotTableGroupBy())
.withColumn(new PivotTableGroupBy())
.withValue(new PivotTableValue())
.withValue(new PivotTableValue())
.withValue(new PivotTableValue())
)));
assertEquals("2 Filters", record.getDisplayValue("queryFilterJson"));
assertEquals("3 Columns", record.getDisplayValue("columnsJson"));
assertEquals("2 Rows, 1 Column, and 3 Values", record.getDisplayValue("pivotTableJson"));
}
{
QRecord record = customize.apply(new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson("blah")
.withColumnsJson("<xml?>")
.withPivotTableJson("{]"));
assertEquals("Invalid Filter...", record.getDisplayValue("queryFilterJson"));
assertEquals("Invalid Columns...", record.getDisplayValue("columnsJson"));
assertEquals("Invalid Pivot Table...", record.getDisplayValue("pivotTableJson"));
}
}
}

View File

@ -0,0 +1,228 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.savedreports;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
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.PivotTableFunction;
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.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Unit test for SavedReportTableCustomizer
*******************************************************************************/
class SavedReportTableCustomizerTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws QException
{
QContext.getQInstance().add(new SavedReportsMetaDataProvider().defineSavedReportTable(TestUtils.MEMORY_BACKEND_NAME, null));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPreInsertAndPreUpdateAreWired() throws QException
{
SavedReport badRecord = new SavedReport()
.withLabel("My Report")
.withTableName("notATable");
/////////////////////////////////////////////////////////////////////
// assertions to apply both to a failed insert and a failed update //
/////////////////////////////////////////////////////////////////////
Consumer<QRecord> asserter = record -> assertThat(record.getErrors())
.hasSizeGreaterThanOrEqualTo(2)
.anyMatch(e -> e.getMessage().contains("Unrecognized table name"))
.anyMatch(e -> e.getMessage().contains("must contain at least 1 column"));
////////////////////////////////////////////////////////////
// go through insert action, to ensure wired-up correctly //
////////////////////////////////////////////////////////////
InsertOutput insertOutput = new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(badRecord));
asserter.accept(insertOutput.getRecords().get(0));
////////////////////////////////
// likewise for update action //
////////////////////////////////
UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(SavedReport.TABLE_NAME).withRecordEntity(badRecord));
asserter.accept(updateOutput.getRecords().get(0));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testParseFails()
{
QRecord record = new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson("...")
.withColumnsJson("x")
.withPivotTableJson("[")
.toQRecord();
new SavedReportTableCustomizer().preValidateRecord(record);
assertThat(record.getErrors())
.hasSize(3)
.anyMatch(e -> e.getMessage().contains("Unable to parse queryFilterJson"))
.anyMatch(e -> e.getMessage().contains("Unable to parse columnsJson"))
.anyMatch(e -> e.getMessage().contains("Unable to parse pivotTableJson"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNoColumns()
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// given a reportColumns object, serialize it to json, put it in a saved report record, and run the pre-validator //
// then assert we got error saying there were no columns. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Consumer<ReportColumns> asserter = reportColumns ->
{
SavedReport savedReport = new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withColumnsJson(JsonUtils.toJson(reportColumns));
QRecord record = savedReport.toQRecord();
new SavedReportTableCustomizer().preValidateRecord(record);
assertThat(record.getErrors())
.hasSize(1)
.anyMatch(e -> e.getMessage().contains("must contain at least 1 column"));
};
asserter.accept(new ReportColumns());
asserter.accept(new ReportColumns().withColumns(null));
asserter.accept(new ReportColumns().withColumns(new ArrayList<>()));
asserter.accept(new ReportColumns().withColumn(new ReportColumn()
.withName("id").withIsVisible(false)));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPivotTables()
{
BiConsumer<PivotTableDefinition, List<String>> asserter = (PivotTableDefinition ptd, List<String> expectedAnyMessageToContain) ->
{
SavedReport savedReport = new SavedReport()
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withColumnsJson(JsonUtils.toJson(new ReportColumns()
.withColumn("id")
.withColumn("firstName")
.withColumn("lastName")
.withColumn("birthDate")))
.withPivotTableJson(JsonUtils.toJson(ptd));
QRecord record = savedReport.toQRecord();
new SavedReportTableCustomizer().preValidateRecord(record);
assertThat(record.getErrors()).hasSize(expectedAnyMessageToContain.size());
for(String expected : expectedAnyMessageToContain)
{
assertThat(record.getErrors())
.anyMatch(e -> e.getMessage().contains(expected));
}
};
asserter.accept(new PivotTableDefinition(), List.of("must contain at least 1 row"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withRow(new PivotTableGroupBy()),
List.of("Missing field name for at least one pivot table row"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withRow(new PivotTableGroupBy().withFieldName("createDate")),
List.of("row is using field (Create Date) which is not an active column"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withColumn(new PivotTableGroupBy()),
List.of("Missing field name for at least one pivot table column"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withColumn(new PivotTableGroupBy().withFieldName("createDate")),
List.of("column is using field (Create Date) which is not an active column"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withValue(new PivotTableValue().withFunction(PivotTableFunction.SUM)),
List.of("Missing field name for at least one pivot table value"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withValue(new PivotTableValue().withFieldName("createDate").withFunction(PivotTableFunction.SUM)),
List.of("value is using field (Create Date) which is not an active column"));
asserter.accept(new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("id"))
.withValue(new PivotTableValue().withFieldName("firstName")),
List.of("Missing function for at least one pivot table value"));
}
}

View File

@ -50,7 +50,7 @@ class BasicRunReportProcessTest extends BaseTest
void testRunReport() throws QException
{
QInstance instance = TestUtils.defineInstance();
QReportMetaData report = GenerateReportActionTest.definePersonShoesPivotReport(true);
QReportMetaData report = GenerateReportActionTest.definePersonShoesSummaryReport(true);
QProcessMetaData runReportProcess = BasicRunReportProcess.defineProcessMetaData();
instance.addReport(report);

View File

@ -0,0 +1,406 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.savedreports;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportActionTest;
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.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableDefinition;
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableFunction;
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.query.QQueryFilter;
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.savedreports.ReportColumns;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryStorageAction;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.LocalMacDevUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.apache.commons.io.IOUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for RenderSavedReportExecuteStep
*******************************************************************************/
class RenderSavedReportProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws Exception
{
new SavedReportsMetaDataProvider().defineAll(QContext.getQInstance(), TestUtils.MEMORY_BACKEND_NAME, TestUtils.MEMORY_BACKEND_NAME, null);
GenerateReportActionTest.insertPersonRecords(QContext.getQInstance());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
@Disabled
void testForDevPrintAPivotDefinitionAsJson()
{
System.out.println(JsonUtils.toPrettyJson(new PivotTableDefinition()
.withRow(new PivotTableGroupBy()
.withFieldName("homeStateId"))
.withRow(new PivotTableGroupBy()
.withFieldName("firstName"))
.withValue(new PivotTableValue()
.withFieldName("id")
.withFunction(PivotTableFunction.COUNT))
.withValue(new PivotTableValue()
.withFieldName("cost")
.withFunction(PivotTableFunction.SUM))
));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testTableOnlyReport() throws Exception
{
String label = "Test Report";
//////////////////////////////////////////////////////////////////////////////////////////
// do columns json as a string, rather than a toJson'ed ReportColumns object, //
// to help verify that we don't choke on un-recognized properties (e.g., as QFMD sends) //
//////////////////////////////////////////////////////////////////////////////////////////
String columnsJson = """
{"columns":[
{"name": "k"},
{"name": "id"},
{"name": "firstName", "isVisible": true},
{"name": "lastName", "pinned": "left"},
{"name": "createDate", "isVisible": false}
]}""";
QRecord savedReport = new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport()
.withLabel(label)
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withColumnsJson(columnsJson)
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
)).getRecords().get(0);
RunProcessOutput runProcessOutput = runRenderReportProcess(savedReport, ReportFormatPossibleValueEnum.CSV);
String downloadFileName = runProcessOutput.getValueString("downloadFileName");
assertThat(downloadFileName)
.startsWith(label + " - ")
.matches(".*\\d\\d\\d\\d-\\d\\d-\\d\\d-\\d\\d\\d\\d.*")
.endsWith(".csv");
InputStream inputStream = getInputStream(runProcessOutput);
List<String> lines = IOUtils.readLines(inputStream, StandardCharsets.UTF_8);
assertEquals("""
"Id","First Name","Last Name"
""".trim(), lines.get(0));
assertEquals("""
"1","Darin","Jonson"
""".trim(), lines.get(1));
writeTmpFileAndOpen(inputStream, ".csv");
}
/*******************************************************************************
**
*******************************************************************************/
private static InputStream getInputStream(RunProcessOutput runProcessOutput) throws QException
{
String storageTableName = runProcessOutput.getValueString("storageTableName");
String storageReference = runProcessOutput.getValueString("storageReference");
InputStream inputStream = new MemoryStorageAction().getInputStream(new StorageInput(storageTableName).withReference(storageReference));
return inputStream;
}
/*******************************************************************************
**
*******************************************************************************/
private void writeTmpFileAndOpen(InputStream inputStream, String suffix) throws IOException
{
// LocalMacDevUtils.mayOpenFiles = true;
if(LocalMacDevUtils.mayOpenFiles)
{
inputStream.reset();
File tmpFile = File.createTempFile(getClass().getName(), suffix, new File("/tmp/"));
FileOutputStream fileOutputStream = new FileOutputStream(tmpFile);
inputStream.transferTo(fileOutputStream);
fileOutputStream.close();
LocalMacDevUtils.openFile(tmpFile.getAbsolutePath());
}
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord insertBasicSavedPivotReport(String label) throws QException
{
return new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport()
.withLabel(label)
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withColumnsJson(JsonUtils.toJson(new ReportColumns()
.withColumn("id")
.withColumn("firstName")
.withColumn("lastName")
.withColumn("cost")
.withColumn("birthDate")
.withColumn("homeStateId")))
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withPivotTableJson(JsonUtils.toJson(new PivotTableDefinition()
.withRow(new PivotTableGroupBy()
.withFieldName("homeStateId"))
.withRow(new PivotTableGroupBy()
.withFieldName("firstName"))
.withValue(new PivotTableValue()
.withFieldName("id")
.withFunction(PivotTableFunction.COUNT))
.withValue(new PivotTableValue()
.withFieldName("cost")
.withFunction(PivotTableFunction.SUM))
.withValue(new PivotTableValue()
.withFieldName("birthDate")
.withFunction(PivotTableFunction.MIN))
.withValue(new PivotTableValue()
.withFieldName("birthDate")
.withFunction(PivotTableFunction.MAX))
))
)).getRecords().get(0);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPivotXlsx() throws Exception
{
String label = "Test Pivot Report";
QRecord savedReport = insertBasicSavedPivotReport(label);
RunProcessOutput runProcessOutput = runRenderReportProcess(savedReport, ReportFormatPossibleValueEnum.XLSX);
InputStream inputStream = getInputStream(runProcessOutput);
writeTmpFileAndOpen(inputStream, ".xlsx");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPivotJson() throws Exception
{
String label = "Test Pivot Report JSON";
QRecord savedReport = insertBasicSavedPivotReport(label);
RunProcessOutput runProcessOutput = runRenderReportProcess(savedReport, ReportFormatPossibleValueEnum.JSON);
InputStream inputStream = getInputStream(runProcessOutput);
String json = StringUtils.join("\n", IOUtils.readLines(inputStream, StandardCharsets.UTF_8));
printReport(json);
JSONArray jsonArray = new JSONArray(json);
assertEquals(2, jsonArray.length());
JSONObject firstView = jsonArray.getJSONObject(0);
assertEquals(label, firstView.getString("name"));
JSONArray firstViewData = firstView.getJSONArray("data");
assertEquals(6, firstViewData.length());
assertThat(firstViewData.getJSONObject(0).toMap())
.hasFieldOrPropertyWithValue("id", 1)
.hasFieldOrPropertyWithValue("firstName", "Darin");
JSONObject pivotView = jsonArray.getJSONObject(1);
assertEquals("Pivot Table", pivotView.getString("name"));
JSONArray pivotViewData = pivotView.getJSONArray("data");
assertEquals(4, pivotViewData.length());
assertThat(pivotViewData.getJSONObject(0).toMap())
.hasFieldOrPropertyWithValue("homeState", "IL")
.hasFieldOrPropertyWithValue("firstName", "Darin")
.hasFieldOrPropertyWithValue("countOfId", 3)
.hasFieldOrPropertyWithValue("sumOfCost", new BigDecimal("1.50"));
assertThat(pivotViewData.getJSONObject(3).toMap())
.hasFieldOrPropertyWithValue("homeState", "Totals")
.hasFieldOrPropertyWithValue("countOfId", 6)
.hasFieldOrPropertyWithValue("sumOfCost", new BigDecimal("12.00"));
}
/*******************************************************************************
**
*******************************************************************************/
private void printReport(String report)
{
// System.out.println(report);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPivotCSV() throws Exception
{
String label = "Test Pivot Report CSV";
QRecord savedReport = insertBasicSavedPivotReport(label);
RunProcessOutput runProcessOutput = runRenderReportProcess(savedReport, ReportFormatPossibleValueEnum.CSV);
InputStream inputStream = getInputStream(runProcessOutput);
List<String> csv = IOUtils.readLines(inputStream, StandardCharsets.UTF_8);
System.out.println(csv);
assertEquals("""
"Home State","First Name","Count Of Id","Sum Of Cost","Min Of Birth Date","Max Of Birth Date\"""", csv.get(0));
assertEquals("""
"Totals","","6","12.00","1979-12-30","1980-03-20\"""", csv.get(4));
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord insertSavedPivotReportWithAllFunctions(String label) throws QException
{
PivotTableDefinition pivotTableDefinition = new PivotTableDefinition()
.withRow(new PivotTableGroupBy().withFieldName("firstName"));
for(PivotTableFunction function : PivotTableFunction.values())
{
pivotTableDefinition.withValue(new PivotTableValue().withFieldName("cost").withFunction(function));
}
return new InsertAction().execute(new InsertInput(SavedReport.TABLE_NAME).withRecordEntity(new SavedReport()
.withLabel(label)
.withTableName(TestUtils.TABLE_NAME_PERSON_MEMORY)
.withColumnsJson(JsonUtils.toJson(new ReportColumns()
.withColumn("id")
.withColumn("firstName")
.withColumn("cost")))
.withQueryFilterJson(JsonUtils.toJson(new QQueryFilter()))
.withPivotTableJson(JsonUtils.toJson(pivotTableDefinition))
)).getRecords().get(0);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPivotXlsxAllFunctions() throws Exception
{
String label = "Test Pivot Report";
QRecord savedReport = insertSavedPivotReportWithAllFunctions(label);
RunProcessOutput runProcessOutput = runRenderReportProcess(savedReport, ReportFormatPossibleValueEnum.XLSX);
String serverFilePath = runProcessOutput.getValueString("serverFilePath");
System.out.println(serverFilePath);
InputStream inputStream = getInputStream(runProcessOutput);
writeTmpFileAndOpen(inputStream, ".xlsx");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testPivotCSVAllFunctions() throws Exception
{
String label = "Test Pivot Report CSV";
QRecord savedReport = insertSavedPivotReportWithAllFunctions(label);
RunProcessOutput runProcessOutput = runRenderReportProcess(savedReport, ReportFormatPossibleValueEnum.CSV);
InputStream inputStream = getInputStream(runProcessOutput);
List<String> csv = IOUtils.readLines(inputStream, StandardCharsets.UTF_8);
System.out.println(csv);
assertEquals("""
"First Name","Average Of Cost","Count Of Cost","Count_nums Of Cost","Max Of Cost","Min Of Cost","Product Of Cost","Std_dev Of Cost","Std_devp Of Cost","Sum Of Cost","Var Of Cost","Varp Of Cost\"""", csv.get(0));
assertEquals("""
"Totals","2.0","6","6","3.50","0.50","5.359375000000","1.6432","1.5000","12.00","2.7000","2.2500\"""", csv.get(4));
}
/*******************************************************************************
**
*******************************************************************************/
private static RunProcessOutput runRenderReportProcess(QRecord savedReport, ReportFormatPossibleValueEnum reportFormat) throws QException
{
RunProcessInput input = new RunProcessInput();
input.setProcessName(RenderSavedReportMetaDataProducer.NAME);
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
input.setCallback(QProcessCallbackFactory.forRecord(savedReport));
input.addValue("reportFormat", reportFormat.getPossibleValueId());
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
return runProcessOutput;
}
}

View File

@ -22,7 +22,6 @@
package com.kingsrook.qqq.backend.core.scheduler;
import java.util.ArrayList;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
@ -32,8 +31,6 @@ import com.kingsrook.qqq.backend.core.logging.QCollectingLogger;
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.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobParameter;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzScheduler;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzTestUtils;
@ -83,29 +80,6 @@ class QScheduleManagerTest extends BaseTest
/*******************************************************************************
**
*******************************************************************************/
private ScheduledJob newScheduledJob(ScheduledJobType type, Map<String, String> params)
{
ScheduledJob scheduledJob = new ScheduledJob()
.withId(1)
.withIsActive(true)
.withSchedulerName(TestUtils.SIMPLE_SCHEDULER_NAME)
.withType(type.name())
.withRepeatSeconds(1)
.withJobParameters(new ArrayList<>());
for(Map.Entry<String, String> entry : params.entrySet())
{
scheduledJob.getJobParameters().add(new ScheduledJobParameter().withKey(entry.getKey()).withValue(entry.getValue()));
}
return (scheduledJob);
}
/*******************************************************************************
**
*******************************************************************************/
@ -114,54 +88,54 @@ class QScheduleManagerTest extends BaseTest
{
QScheduleManager qScheduleManager = QScheduleManager.initInstance(QContext.getQInstance(), () -> QContext.getQSession());
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.PROCESS, Map.of()).withRepeatSeconds(null)))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.PROCESS, Map.of()).withRepeatSeconds(null)))
.hasMessageContaining("Missing a schedule");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.PROCESS, Map.of()).withType(null)))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.PROCESS, Map.of()).withType(null)))
.hasMessageContaining("Missing a type");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.PROCESS, Map.of()).withType("notAType")))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.PROCESS, Map.of()).withType("notAType")))
.hasMessageContaining("Unrecognized type");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.PROCESS, Map.of())))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.PROCESS, Map.of())))
.hasMessageContaining("Missing scheduledJobParameter with key [processName]");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.PROCESS, Map.of("processName", "notAProcess"))))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.PROCESS, Map.of("processName", "notAProcess"))))
.hasMessageContaining("Unrecognized processName");
QContext.getQInstance().getProcess(TestUtils.PROCESS_NAME_BASEPULL).withSchedule(new QScheduleMetaData());
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.PROCESS, Map.of("processName", TestUtils.PROCESS_NAME_BASEPULL))))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.PROCESS, Map.of("processName", TestUtils.PROCESS_NAME_BASEPULL))))
.hasMessageContaining("has a schedule in its metaData - so it should not be dynamically scheduled");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.QUEUE_PROCESSOR, Map.of())))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.QUEUE_PROCESSOR, Map.of())))
.hasMessageContaining("Missing scheduledJobParameter with key [queueName]");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.QUEUE_PROCESSOR, Map.of("queueName", "notAQueue"))))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.QUEUE_PROCESSOR, Map.of("queueName", "notAQueue"))))
.hasMessageContaining("Unrecognized queueName");
QContext.getQInstance().getQueue(TestUtils.TEST_SQS_QUEUE).withSchedule(new QScheduleMetaData());
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.QUEUE_PROCESSOR, Map.of("queueName", TestUtils.TEST_SQS_QUEUE))))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.QUEUE_PROCESSOR, Map.of("queueName", TestUtils.TEST_SQS_QUEUE))))
.hasMessageContaining("has a schedule in its metaData - so it should not be dynamically scheduled");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of())))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of())))
.hasMessageContaining("Missing scheduledJobParameter with key [tableName]");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", "notATable"))))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", "notATable"))))
.hasMessageContaining("Missing scheduledJobParameter with key [automationStatus]");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", "notATable", "automationStatus", AutomationStatus.PENDING_INSERT_AUTOMATIONS.name()))))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", "notATable", "automationStatus", AutomationStatus.PENDING_INSERT_AUTOMATIONS.name()))))
.hasMessageContaining("Unrecognized tableName");
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC, "automationStatus", AutomationStatus.PENDING_INSERT_AUTOMATIONS.name()))))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC, "automationStatus", AutomationStatus.PENDING_INSERT_AUTOMATIONS.name()))))
.hasMessageContaining("does not have automationDetails");
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().withSchedule(null);
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY, "automationStatus", "foobar"))))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY, "automationStatus", "foobar"))))
.hasMessageContaining("Did not find table automation actions matching automationStatus")
.hasMessageContaining("Found: PENDING_INSERT_AUTOMATIONS,PENDING_UPDATE_AUTOMATIONS");
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().withSchedule(new QScheduleMetaData());
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY, "automationStatus", AutomationStatus.PENDING_INSERT_AUTOMATIONS.name()))))
assertThatThrownBy(() -> qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS, Map.of("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY, "automationStatus", AutomationStatus.PENDING_INSERT_AUTOMATIONS.name()))))
.hasMessageContaining("has a schedule in its metaData - so it should not be dynamically scheduled");
}
@ -181,19 +155,19 @@ class QScheduleManagerTest extends BaseTest
QScheduleManager qScheduleManager = QScheduleManager.initInstance(qInstance, () -> QContext.getQSession());
qScheduleManager.start();
qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.PROCESS,
qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.PROCESS,
Map.of("processName", TestUtils.PROCESS_NAME_GREET_PEOPLE))
.withId(2)
.withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME));
qInstance.getQueue(TestUtils.TEST_SQS_QUEUE).setSchedule(null);
qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.QUEUE_PROCESSOR,
qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.QUEUE_PROCESSOR,
Map.of("queueName", TestUtils.TEST_SQS_QUEUE))
.withId(3)
.withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME));
qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getAutomationDetails().setSchedule(null);
qScheduleManager.setupScheduledJob(newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS,
qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.TABLE_AUTOMATIONS,
Map.of("tableName", TestUtils.TABLE_NAME_PERSON_MEMORY, "automationStatus", AutomationStatus.PENDING_UPDATE_AUTOMATIONS.name()))
.withId(4)
.withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME));

View File

@ -22,7 +22,9 @@
package com.kingsrook.qqq.backend.core.scheduler;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
@ -31,6 +33,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJob;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobParameter;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
/*******************************************************************************
@ -57,6 +63,28 @@ public class SchedulerTestUtils
/*******************************************************************************
**
*******************************************************************************/
public static ScheduledJob newScheduledJob(ScheduledJobType type, Map<String, String> params)
{
ScheduledJob scheduledJob = new ScheduledJob()
.withId(1)
.withIsActive(true)
.withSchedulerName(TestUtils.SIMPLE_SCHEDULER_NAME)
.withType(type.name())
.withRepeatSeconds(1)
.withJobParameters(new ArrayList<>());
for(Map.Entry<String, String> entry : params.entrySet())
{
scheduledJob.getJobParameters().add(new ScheduledJobParameter().withKey(entry.getKey()).withValue(entry.getValue()));
}
return (scheduledJob);
}
/*******************************************************************************
**

View File

@ -0,0 +1,128 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.scheduler.processes;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.context.QContext;
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.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.scheduler.SchedulerTestUtils;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzJobAndTriggerWrapper;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzScheduler;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzTestUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.quartz.SchedulerException;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for RescheduleAllJobsProcess
*******************************************************************************/
class RescheduleAllJobsProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void afterEach()
{
QLogger.deactivateCollectingLoggerForClass(QuartzScheduler.class);
try
{
QScheduleManager.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
try
{
QuartzScheduler.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException, SchedulerException
{
QInstance qInstance = QContext.getQInstance();
MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, RescheduleAllJobsProcess.class.getPackageName());
QuartzTestUtils.setupInstanceForQuartzTests();
QScheduleManager qScheduleManager = QScheduleManager.initInstance(qInstance, () -> QContext.getQSession());
qScheduleManager.start();
qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.PROCESS,
Map.of("processName", TestUtils.PROCESS_NAME_GREET_PEOPLE))
.withId(2)
.withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME));
QuartzScheduler quartzScheduler = QuartzScheduler.getInstance();
List<QuartzJobAndTriggerWrapper> wrappers = quartzScheduler.queryQuartz();
///////////////////////////////////////////////////////////////
// make sure our scheduledJob here got scheduled with quartz //
///////////////////////////////////////////////////////////////
assertTrue(wrappers.stream().anyMatch(w -> w.jobDetail().getKey().getName().equals("scheduledJob:2")));
/////////////////////////
// run the re-schedule //
/////////////////////////
RunProcessInput input = new RunProcessInput();
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
input.setProcessName(RescheduleAllJobsProcess.class.getSimpleName());
new RunProcessAction().execute(input);
////////////////////////////////////////////////////////////////////////////////////////
// now, because our scheduled job record isn't actually stored in ScheduledJob table, //
// when we reschdule all, it should become unscheduled. //
////////////////////////////////////////////////////////////////////////////////////////
wrappers = quartzScheduler.queryQuartz();
assertTrue(wrappers.stream().noneMatch(w -> w.jobDetail().getKey().getName().equals("scheduledJob:2")));
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.scheduler.processes;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.context.QContext;
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.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobType;
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
import com.kingsrook.qqq.backend.core.scheduler.SchedulerTestUtils;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzJobAndTriggerWrapper;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzScheduler;
import com.kingsrook.qqq.backend.core.scheduler.quartz.QuartzTestUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.quartz.SchedulerException;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for UnscheduleAllJobsProcess
*******************************************************************************/
class UnscheduleAllJobsProcessTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void afterEach()
{
QLogger.deactivateCollectingLoggerForClass(QuartzScheduler.class);
try
{
QScheduleManager.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
try
{
QuartzScheduler.getInstance().unInit();
}
catch(IllegalStateException ise)
{
/////////////////////////////////////////////////////////////////
// ok, might just mean that this test didn't init the instance //
/////////////////////////////////////////////////////////////////
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException, SchedulerException
{
QInstance qInstance = QContext.getQInstance();
MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, UnscheduleAllJobsProcess.class.getPackageName());
QuartzTestUtils.setupInstanceForQuartzTests();
QScheduleManager qScheduleManager = QScheduleManager.initInstance(qInstance, () -> QContext.getQSession());
qScheduleManager.start();
qScheduleManager.setupScheduledJob(SchedulerTestUtils.newScheduledJob(ScheduledJobType.PROCESS,
Map.of("processName", TestUtils.PROCESS_NAME_GREET_PEOPLE))
.withId(2)
.withSchedulerName(QuartzTestUtils.QUARTZ_SCHEDULER_NAME));
QuartzScheduler quartzScheduler = QuartzScheduler.getInstance();
List<QuartzJobAndTriggerWrapper> wrappers = quartzScheduler.queryQuartz();
assertTrue(wrappers.stream().anyMatch(w -> w.jobDetail().getKey().getName().equals("scheduledJob:2")));
RunProcessInput input = new RunProcessInput();
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
input.setProcessName(UnscheduleAllJobsProcess.class.getSimpleName());
new RunProcessAction().execute(input);
wrappers = quartzScheduler.queryQuartz();
assertTrue(wrappers.stream().noneMatch(w -> w.jobDetail().getKey().getName().equals("scheduledJob:2")));
}
}

View File

@ -40,6 +40,14 @@ public class PersonQRecord extends QRecord
public PersonQRecord withFirstName(String firstName)
{
setValue("firstName", firstName);
return (this);
}
public PersonQRecord withBirthDate(LocalDate birthDate)
{
setValue("birthDate", birthDate);

View File

@ -25,7 +25,9 @@ package com.kingsrook.qqq.backend.core.utils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
@ -79,4 +81,20 @@ class ObjectUtilsTest
assertEquals("else", ObjectUtils.tryAndRequireNonNullElse(() -> null, "else"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testIfCan()
{
Object nullObject = null;
assertTrue(ObjectUtils.ifCan(() -> true));
assertTrue(ObjectUtils.ifCan(() -> "a".equals("a")));
assertFalse(ObjectUtils.ifCan(() -> 1 == 2));
assertFalse(ObjectUtils.ifCan(() -> nullObject.equals("a")));
assertFalse(ObjectUtils.ifCan(() -> null));
}
}

View File

@ -23,6 +23,9 @@ package com.kingsrook.qqq.backend.core.utils.aggregates;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.Month;
import com.kingsrook.qqq.backend.core.BaseTest;
import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;
@ -78,6 +81,12 @@ class AggregatesTest extends BaseTest
assertEquals(15, aggregates.getMax());
assertEquals(30, aggregates.getSum());
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10"), Offset.offset(BigDecimal.ZERO));
assertEquals(new BigDecimal("750"), aggregates.getProduct());
assertEquals(new BigDecimal("25.0000"), aggregates.getVariance());
assertEquals(new BigDecimal("5.0000"), aggregates.getStandardDeviation());
assertThat(aggregates.getVarP()).isCloseTo(new BigDecimal("16.6667"), Offset.offset(new BigDecimal(".0001")));
assertThat(aggregates.getStdDevP()).isCloseTo(new BigDecimal("4.0824"), Offset.offset(new BigDecimal(".0001")));
}
@ -89,6 +98,7 @@ class AggregatesTest extends BaseTest
void testBigDecimal()
{
BigDecimalAggregates aggregates = new BigDecimalAggregates();
aggregates.add(null);
assertEquals(0, aggregates.getCount());
assertNull(aggregates.getMin());
@ -114,13 +124,117 @@ class AggregatesTest extends BaseTest
BigDecimal bd148 = new BigDecimal("14.8");
aggregates.add(bd148);
aggregates.add(null);
assertEquals(3, aggregates.getCount());
assertEquals(bd51, aggregates.getMin());
assertEquals(bd148, aggregates.getMax());
assertEquals(new BigDecimal("30.0"), aggregates.getSum());
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10.0"), Offset.offset(BigDecimal.ZERO));
assertEquals(new BigDecimal("762.348"), aggregates.getProduct());
assertEquals(new BigDecimal("23.5300"), aggregates.getVariance());
assertEquals(new BigDecimal("4.8508"), aggregates.getStandardDeviation());
assertThat(aggregates.getVarP()).isCloseTo(new BigDecimal("15.6867"), Offset.offset(new BigDecimal(".0001")));
assertThat(aggregates.getStdDevP()).isCloseTo(new BigDecimal("3.9606"), Offset.offset(new BigDecimal(".0001")));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testInstant()
{
InstantAggregates aggregates = new InstantAggregates();
assertEquals(0, aggregates.getCount());
assertNull(aggregates.getMin());
assertNull(aggregates.getMax());
assertNull(aggregates.getSum());
assertNull(aggregates.getAverage());
Instant i1970 = Instant.parse("1970-01-01T00:00:00Z");
aggregates.add(i1970);
assertEquals(1, aggregates.getCount());
assertEquals(i1970, aggregates.getMin());
assertEquals(i1970, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(i1970, aggregates.getAverage());
Instant i1980 = Instant.parse("1980-01-01T00:00:00Z");
aggregates.add(i1980);
assertEquals(2, aggregates.getCount());
assertEquals(i1970, aggregates.getMin());
assertEquals(i1980, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(Instant.parse("1975-01-01T00:00:00Z"), aggregates.getAverage());
Instant i1990 = Instant.parse("1990-01-01T00:00:00Z");
aggregates.add(i1990);
assertEquals(3, aggregates.getCount());
assertEquals(i1970, aggregates.getMin());
assertEquals(i1990, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(Instant.parse("1980-01-01T08:00:00Z"), aggregates.getAverage()); // a leap day throws this off by 8 hours :)
/////////////////////////////////////////////////////////////////////
// assert we gracefully return null for these ops we don't support //
/////////////////////////////////////////////////////////////////////
assertNull(aggregates.getProduct());
assertNull(aggregates.getVariance());
assertNull(aggregates.getStandardDeviation());
assertNull(aggregates.getVarP());
assertNull(aggregates.getStdDevP());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testLocalDate()
{
LocalDateAggregates aggregates = new LocalDateAggregates();
assertEquals(0, aggregates.getCount());
assertNull(aggregates.getMin());
assertNull(aggregates.getMax());
assertNull(aggregates.getSum());
assertNull(aggregates.getAverage());
LocalDate ld1970 = LocalDate.of(1970, Month.JANUARY, 1);
aggregates.add(ld1970);
assertEquals(1, aggregates.getCount());
assertEquals(ld1970, aggregates.getMin());
assertEquals(ld1970, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(ld1970, aggregates.getAverage());
LocalDate ld1980 = LocalDate.of(1980, Month.JANUARY, 1);
aggregates.add(ld1980);
assertEquals(2, aggregates.getCount());
assertEquals(ld1970, aggregates.getMin());
assertEquals(ld1980, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(LocalDate.of(1975, Month.JANUARY, 1), aggregates.getAverage());
LocalDate ld1990 = LocalDate.of(1990, Month.JANUARY, 1);
aggregates.add(ld1990);
assertEquals(3, aggregates.getCount());
assertEquals(ld1970, aggregates.getMin());
assertEquals(ld1990, aggregates.getMax());
assertNull(aggregates.getSum());
assertEquals(ld1980, aggregates.getAverage());
/////////////////////////////////////////////////////////////////////
// assert we gracefully return null for these ops we don't support //
/////////////////////////////////////////////////////////////////////
assertNull(aggregates.getProduct());
assertNull(aggregates.getVariance());
assertNull(aggregates.getStandardDeviation());
assertNull(aggregates.getVarP());
assertNull(aggregates.getStdDevP());
}
}