mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
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:
@ -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>
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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))));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
{
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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("&");
|
||||
}
|
||||
else if(c == '<')
|
||||
{
|
||||
rs.append("<");
|
||||
}
|
||||
else if(c == '>')
|
||||
{
|
||||
rs.append(">");
|
||||
}
|
||||
else if(c == '\'')
|
||||
{
|
||||
rs.append("'");
|
||||
}
|
||||
else if(c == '"')
|
||||
{
|
||||
rs.append(""");
|
||||
}
|
||||
else if (c < 32 && c != '\t' && c != '\n')
|
||||
{
|
||||
rs.append(' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
rs.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
Map<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>");
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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++;
|
||||
|
||||
////////////////////////////////////////////
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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...)
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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 //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)");
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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...
|
||||
}
|
||||
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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 ")
|
||||
|
@ -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) ->
|
||||
{
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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, () ->
|
||||
{
|
||||
|
@ -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");
|
||||
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
**
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
|
@ -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")));
|
||||
}
|
||||
|
||||
}
|
@ -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")));
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user