mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 21:20:45 +00:00
Merge branch 'feature/QQQ-42-reports' into feature/sprint-11
This commit is contained in:
@ -27,24 +27,25 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.kingsrook.qqq.backend.core.adapters.QRecordToCsvAdapter;
|
import com.kingsrook.qqq.backend.core.adapters.QRecordToCsvAdapter;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
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.model.metadata.tables.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** CSV report format implementation
|
** CSV export format implementation
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class CsvReportStreamer implements ReportStreamerInterface
|
public class CsvExportStreamer implements ExportStreamerInterface
|
||||||
{
|
{
|
||||||
private static final Logger LOG = LogManager.getLogger(CsvReportStreamer.class);
|
private static final Logger LOG = LogManager.getLogger(CsvExportStreamer.class);
|
||||||
|
|
||||||
private final QRecordToCsvAdapter qRecordToCsvAdapter;
|
private final QRecordToCsvAdapter qRecordToCsvAdapter;
|
||||||
|
|
||||||
private ReportInput reportInput;
|
private ExportInput exportInput;
|
||||||
private QTableMetaData table;
|
private QTableMetaData table;
|
||||||
private List<QFieldMetaData> fields;
|
private List<QFieldMetaData> fields;
|
||||||
private OutputStream outputStream;
|
private OutputStream outputStream;
|
||||||
@ -54,7 +55,7 @@ public class CsvReportStreamer implements ReportStreamerInterface
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public CsvReportStreamer()
|
public CsvExportStreamer()
|
||||||
{
|
{
|
||||||
qRecordToCsvAdapter = new QRecordToCsvAdapter();
|
qRecordToCsvAdapter = new QRecordToCsvAdapter();
|
||||||
}
|
}
|
||||||
@ -65,12 +66,12 @@ public class CsvReportStreamer implements ReportStreamerInterface
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@Override
|
||||||
public void start(ReportInput reportInput, List<QFieldMetaData> fields) throws QReportingException
|
public void start(ExportInput exportInput, List<QFieldMetaData> fields) throws QReportingException
|
||||||
{
|
{
|
||||||
this.reportInput = reportInput;
|
this.exportInput = exportInput;
|
||||||
this.fields = fields;
|
this.fields = fields;
|
||||||
table = reportInput.getTable();
|
table = exportInput.getTable();
|
||||||
outputStream = this.reportInput.getReportOutputStream();
|
outputStream = this.exportInput.getReportOutputStream();
|
||||||
|
|
||||||
writeReportHeaderRow();
|
writeReportHeaderRow();
|
||||||
}
|
}
|
||||||
@ -84,6 +85,11 @@ public class CsvReportStreamer implements ReportStreamerInterface
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if(StringUtils.hasContent(exportInput.getTitleRow()))
|
||||||
|
{
|
||||||
|
outputStream.write(exportInput.getTitleRow().getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
int col = 0;
|
int col = 0;
|
||||||
for(QFieldMetaData column : fields)
|
for(QFieldMetaData column : fields)
|
||||||
{
|
{
|
||||||
@ -113,16 +119,26 @@ public class CsvReportStreamer implements ReportStreamerInterface
|
|||||||
List<QRecord> qRecords = recordPipe.consumeAvailableRecords();
|
List<QRecord> qRecords = recordPipe.consumeAvailableRecords();
|
||||||
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
for(QRecord qRecord : qRecords)
|
for(QRecord qRecord : qRecords)
|
||||||
|
{
|
||||||
|
writeRecord(qRecord);
|
||||||
|
}
|
||||||
|
return (qRecords.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void writeRecord(QRecord qRecord) throws QReportingException
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
String csv = qRecordToCsvAdapter.recordToCsv(table, qRecord, fields);
|
String csv = qRecordToCsvAdapter.recordToCsv(table, qRecord, fields);
|
||||||
outputStream.write(csv.getBytes(StandardCharsets.UTF_8));
|
outputStream.write(csv.getBytes(StandardCharsets.UTF_8));
|
||||||
outputStream.flush(); // todo - less often?
|
outputStream.flush(); // todo - less often?
|
||||||
}
|
}
|
||||||
return (qRecords.size());
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
throw (new QReportingException("Error writing CSV report", e));
|
throw (new QReportingException("Error writing CSV report", e));
|
||||||
@ -131,6 +147,17 @@ public class CsvReportStreamer implements ReportStreamerInterface
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void addTotalsRow(QRecord record) throws QReportingException
|
||||||
|
{
|
||||||
|
writeRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
@ -28,41 +28,49 @@ import java.time.LocalDate;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.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.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.dhatim.fastexcel.BorderSide;
|
||||||
|
import org.dhatim.fastexcel.BorderStyle;
|
||||||
import org.dhatim.fastexcel.Workbook;
|
import org.dhatim.fastexcel.Workbook;
|
||||||
import org.dhatim.fastexcel.Worksheet;
|
import org.dhatim.fastexcel.Worksheet;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Excel report format implementation
|
** Excel export format implementation
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class ExcelReportStreamer implements ReportStreamerInterface
|
public class ExcelExportStreamer implements ExportStreamerInterface
|
||||||
{
|
{
|
||||||
private static final Logger LOG = LogManager.getLogger(ExcelReportStreamer.class);
|
private static final Logger LOG = LogManager.getLogger(ExcelExportStreamer.class);
|
||||||
|
|
||||||
private ReportInput reportInput;
|
private ExportInput exportInput;
|
||||||
private QTableMetaData table;
|
private QTableMetaData table;
|
||||||
private List<QFieldMetaData> fields;
|
private List<QFieldMetaData> fields;
|
||||||
private OutputStream outputStream;
|
private OutputStream outputStream;
|
||||||
|
|
||||||
|
private Map<String, String> excelCellFormats;
|
||||||
|
|
||||||
private Workbook workbook;
|
private Workbook workbook;
|
||||||
private Worksheet worksheet;
|
private Worksheet worksheet;
|
||||||
private int row = 1;
|
private int row = 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public ExcelReportStreamer()
|
public ExcelExportStreamer()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,12 +80,31 @@ public class ExcelReportStreamer implements ReportStreamerInterface
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@Override
|
||||||
public void start(ReportInput reportInput, List<QFieldMetaData> fields) throws QReportingException
|
public void setDisplayFormats(Map<String, String> displayFormats)
|
||||||
{
|
{
|
||||||
this.reportInput = reportInput;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void start(ExportInput exportInput, List<QFieldMetaData> fields) throws QReportingException
|
||||||
|
{
|
||||||
|
this.exportInput = exportInput;
|
||||||
this.fields = fields;
|
this.fields = fields;
|
||||||
table = reportInput.getTable();
|
table = exportInput.getTable();
|
||||||
outputStream = this.reportInput.getReportOutputStream();
|
outputStream = this.exportInput.getReportOutputStream();
|
||||||
|
|
||||||
workbook = new Workbook(outputStream, "QQQ", null);
|
workbook = new Workbook(outputStream, "QQQ", null);
|
||||||
worksheet = workbook.newWorksheet("Sheet 1");
|
worksheet = workbook.newWorksheet("Sheet 1");
|
||||||
@ -94,13 +121,32 @@ public class ExcelReportStreamer implements ReportStreamerInterface
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if(StringUtils.hasContent(exportInput.getTitleRow()))
|
||||||
|
{
|
||||||
|
worksheet.value(row, 0, exportInput.getTitleRow());
|
||||||
|
worksheet.range(row, 0, row, fields.size() - 1).merge();
|
||||||
|
worksheet.range(row, 0, row, fields.size() - 1).style()
|
||||||
|
.bold()
|
||||||
|
.fontSize(14)
|
||||||
|
.horizontalAlignment("center")
|
||||||
|
.set();
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
int col = 0;
|
int col = 0;
|
||||||
for(QFieldMetaData column : fields)
|
for(QFieldMetaData column : fields)
|
||||||
{
|
{
|
||||||
worksheet.value(0, col, column.getLabel());
|
worksheet.value(row, col, column.getLabel());
|
||||||
col++;
|
col++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
worksheet.range(row, 0, row, fields.size() - 1).style()
|
||||||
|
.bold()
|
||||||
|
.borderStyle(BorderSide.BOTTOM, BorderStyle.THIN)
|
||||||
|
.set();
|
||||||
|
|
||||||
|
row++;
|
||||||
|
|
||||||
worksheet.flush();
|
worksheet.flush();
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
@ -124,10 +170,39 @@ public class ExcelReportStreamer implements ReportStreamerInterface
|
|||||||
{
|
{
|
||||||
for(QRecord qRecord : qRecords)
|
for(QRecord qRecord : qRecords)
|
||||||
{
|
{
|
||||||
int col = 0;
|
writeRecord(qRecord);
|
||||||
for(QFieldMetaData column : fields)
|
|
||||||
|
row++;
|
||||||
|
worksheet.flush(); // todo? not at all? or just sometimes?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
Serializable value = qRecord.getValue(column.getName());
|
try
|
||||||
|
{
|
||||||
|
workbook.finish();
|
||||||
|
outputStream.close();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
throw (new QReportingException("Error generating Excel report", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (qRecords.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void writeRecord(QRecord qRecord)
|
||||||
|
{
|
||||||
|
int col = 0;
|
||||||
|
for(QFieldMetaData field : fields)
|
||||||
|
{
|
||||||
|
Serializable value = qRecord.getValue(field.getName());
|
||||||
if(value != null)
|
if(value != null)
|
||||||
{
|
{
|
||||||
if(value instanceof String s)
|
if(value instanceof String s)
|
||||||
@ -137,6 +212,15 @@ public class ExcelReportStreamer implements ReportStreamerInterface
|
|||||||
else if(value instanceof Number n)
|
else if(value instanceof Number n)
|
||||||
{
|
{
|
||||||
worksheet.value(row, col, n);
|
worksheet.value(row, col, n);
|
||||||
|
|
||||||
|
if(excelCellFormats != null)
|
||||||
|
{
|
||||||
|
String format = excelCellFormats.get(field.getName());
|
||||||
|
if(format != null)
|
||||||
|
{
|
||||||
|
worksheet.style(row, col).format(format).set();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if(value instanceof Boolean b)
|
else if(value instanceof Boolean b)
|
||||||
{
|
{
|
||||||
@ -169,25 +253,22 @@ public class ExcelReportStreamer implements ReportStreamerInterface
|
|||||||
}
|
}
|
||||||
col++;
|
col++;
|
||||||
}
|
}
|
||||||
|
|
||||||
row++;
|
|
||||||
worksheet.flush(); // todo? not at all? or just sometimes?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
workbook.finish();
|
|
||||||
outputStream.close();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
throw (new QReportingException("Error generating Excel report", e));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (qRecords.size());
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void addTotalsRow(QRecord record)
|
||||||
|
{
|
||||||
|
writeRecord(record);
|
||||||
|
|
||||||
|
worksheet.range(row, 0, row, fields.size() - 1).style()
|
||||||
|
.bold()
|
||||||
|
.borderStyle(BorderSide.TOP, BorderStyle.THIN)
|
||||||
|
.borderStyle(BorderSide.BOTTOM, BorderStyle.DOUBLE)
|
||||||
|
.set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,9 +35,9 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
|
|||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.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.CountInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||||
@ -53,7 +53,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Action to generate a report.
|
** Action to generate an export from a table
|
||||||
**
|
**
|
||||||
** At this time (future may change?), this action starts a new thread to run
|
** At this time (future may change?), this action starts a new thread to run
|
||||||
** the query in the backend module. As records are produced by the query,
|
** the query in the backend module. As records are produced by the query,
|
||||||
@ -63,9 +63,9 @@ import org.apache.logging.log4j.Logger;
|
|||||||
** time the report outputStream can be closed.
|
** time the report outputStream can be closed.
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class ReportAction
|
public class ExportAction
|
||||||
{
|
{
|
||||||
private static final Logger LOG = LogManager.getLogger(ReportAction.class);
|
private static final Logger LOG = LogManager.getLogger(ExportAction.class);
|
||||||
|
|
||||||
private boolean preExecuteRan = false;
|
private boolean preExecuteRan = false;
|
||||||
private Integer countFromPreExecute = null;
|
private Integer countFromPreExecute = null;
|
||||||
@ -82,21 +82,21 @@ public class ReportAction
|
|||||||
** first, in their thread, to catch any validation errors before they start
|
** first, in their thread, to catch any validation errors before they start
|
||||||
** the thread (which they may abandon).
|
** the thread (which they may abandon).
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void preExecute(ReportInput reportInput) throws QException
|
public void preExecute(ExportInput exportInput) throws QException
|
||||||
{
|
{
|
||||||
ActionHelper.validateSession(reportInput);
|
ActionHelper.validateSession(exportInput);
|
||||||
|
|
||||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||||
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(reportInput.getBackend());
|
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(exportInput.getBackend());
|
||||||
|
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
// verify field names (if given) //
|
// verify field names (if given) //
|
||||||
///////////////////////////////////
|
///////////////////////////////////
|
||||||
if(CollectionUtils.nullSafeHasContents(reportInput.getFieldNames()))
|
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
|
||||||
{
|
{
|
||||||
QTableMetaData table = reportInput.getTable();
|
QTableMetaData table = exportInput.getTable();
|
||||||
List<String> badFieldNames = new ArrayList<>();
|
List<String> badFieldNames = new ArrayList<>();
|
||||||
for(String fieldName : reportInput.getFieldNames())
|
for(String fieldName : exportInput.getFieldNames())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -119,8 +119,8 @@ public class ReportAction
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// check if this report format has a max-rows limit -- if so, do a count to verify we're under the limit //
|
// check if this report format has a max-rows limit -- if so, do a count to verify we're under the limit //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
ReportFormat reportFormat = reportInput.getReportFormat();
|
ReportFormat reportFormat = exportInput.getReportFormat();
|
||||||
verifyCountUnderMax(reportInput, backendModule, reportFormat);
|
verifyCountUnderMax(exportInput, backendModule, reportFormat);
|
||||||
|
|
||||||
preExecuteRan = true;
|
preExecuteRan = true;
|
||||||
}
|
}
|
||||||
@ -130,28 +130,28 @@ public class ReportAction
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Run the report.
|
** Run the report.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public ReportOutput execute(ReportInput reportInput) throws QException
|
public ExportOutput execute(ExportInput exportInput) throws QException
|
||||||
{
|
{
|
||||||
if(!preExecuteRan)
|
if(!preExecuteRan)
|
||||||
{
|
{
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
// ensure that pre-execute has ran //
|
// ensure that pre-execute has ran //
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
preExecute(reportInput);
|
preExecute(exportInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||||
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(reportInput.getBackend());
|
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(exportInput.getBackend());
|
||||||
|
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
// set up a query input //
|
// set up a query input //
|
||||||
//////////////////////////
|
//////////////////////////
|
||||||
QueryInterface queryInterface = backendModule.getQueryInterface();
|
QueryInterface queryInterface = backendModule.getQueryInterface();
|
||||||
QueryInput queryInput = new QueryInput(reportInput.getInstance());
|
QueryInput queryInput = new QueryInput(exportInput.getInstance());
|
||||||
queryInput.setSession(reportInput.getSession());
|
queryInput.setSession(exportInput.getSession());
|
||||||
queryInput.setTableName(reportInput.getTableName());
|
queryInput.setTableName(exportInput.getTableName());
|
||||||
queryInput.setFilter(reportInput.getQueryFilter());
|
queryInput.setFilter(exportInput.getQueryFilter());
|
||||||
queryInput.setLimit(reportInput.getLimit());
|
queryInput.setLimit(exportInput.getLimit());
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////
|
||||||
// tell this query that it needs to put its output into a pipe //
|
// tell this query that it needs to put its output into a pipe //
|
||||||
@ -162,9 +162,9 @@ public class ReportAction
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// set up a report streamer, which will read rows from the pipe, and write formatted report rows to the output stream //
|
// set up a report streamer, which will read rows from the pipe, and write formatted report rows to the output stream //
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
ReportFormat reportFormat = reportInput.getReportFormat();
|
ReportFormat reportFormat = exportInput.getReportFormat();
|
||||||
ReportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
|
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
|
||||||
reportStreamer.start(reportInput, getFields(reportInput));
|
reportStreamer.start(exportInput, getFields(exportInput));
|
||||||
|
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
// run the query action as an async job //
|
// run the query action as an async job //
|
||||||
@ -251,17 +251,17 @@ public class ReportAction
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
reportInput.getReportOutputStream().close();
|
exportInput.getReportOutputStream().close();
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
throw (new QReportingException("Error completing report", e));
|
throw (new QReportingException("Error completing report", e));
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportOutput reportOutput = new ReportOutput();
|
ExportOutput exportOutput = new ExportOutput();
|
||||||
reportOutput.setRecordCount(recordCount);
|
exportOutput.setRecordCount(recordCount);
|
||||||
|
|
||||||
return (reportOutput);
|
return (exportOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -269,12 +269,12 @@ public class ReportAction
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private List<QFieldMetaData> getFields(ReportInput reportInput)
|
private List<QFieldMetaData> getFields(ExportInput exportInput)
|
||||||
{
|
{
|
||||||
QTableMetaData table = reportInput.getTable();
|
QTableMetaData table = exportInput.getTable();
|
||||||
if(reportInput.getFieldNames() != null)
|
if(exportInput.getFieldNames() != null)
|
||||||
{
|
{
|
||||||
return (reportInput.getFieldNames().stream().map(table::getField).toList());
|
return (exportInput.getFieldNames().stream().map(table::getField).toList());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -287,12 +287,12 @@ public class ReportAction
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private void verifyCountUnderMax(ReportInput reportInput, QBackendModuleInterface backendModule, ReportFormat reportFormat) throws QException
|
private void verifyCountUnderMax(ExportInput exportInput, QBackendModuleInterface backendModule, ReportFormat reportFormat) throws QException
|
||||||
{
|
{
|
||||||
if(reportFormat.getMaxCols() != null)
|
if(reportFormat.getMaxCols() != null)
|
||||||
{
|
{
|
||||||
List<QFieldMetaData> fields = getFields(reportInput);
|
List<QFieldMetaData> fields = getFields(exportInput);
|
||||||
if (fields.size() > reportFormat.getMaxCols())
|
if(fields.size() > reportFormat.getMaxCols())
|
||||||
{
|
{
|
||||||
throw (new QUserFacingException("The requested report would include more columns ("
|
throw (new QUserFacingException("The requested report would include more columns ("
|
||||||
+ String.format("%,d", fields.size()) + ") than the maximum allowed ("
|
+ String.format("%,d", fields.size()) + ") than the maximum allowed ("
|
||||||
@ -302,13 +302,13 @@ public class ReportAction
|
|||||||
|
|
||||||
if(reportFormat.getMaxRows() != null)
|
if(reportFormat.getMaxRows() != null)
|
||||||
{
|
{
|
||||||
if(reportInput.getLimit() == null || reportInput.getLimit() > reportFormat.getMaxRows())
|
if(exportInput.getLimit() == null || exportInput.getLimit() > reportFormat.getMaxRows())
|
||||||
{
|
{
|
||||||
CountInterface countInterface = backendModule.getCountInterface();
|
CountInterface countInterface = backendModule.getCountInterface();
|
||||||
CountInput countInput = new CountInput(reportInput.getInstance());
|
CountInput countInput = new CountInput(exportInput.getInstance());
|
||||||
countInput.setSession(reportInput.getSession());
|
countInput.setSession(exportInput.getSession());
|
||||||
countInput.setTableName(reportInput.getTableName());
|
countInput.setTableName(exportInput.getTableName());
|
||||||
countInput.setFilter(reportInput.getQueryFilter());
|
countInput.setFilter(exportInput.getQueryFilter());
|
||||||
CountOutput countOutput = countInterface.execute(countInput);
|
CountOutput countOutput = countInterface.execute(countInput);
|
||||||
countFromPreExecute = countOutput.getCount();
|
countFromPreExecute = countOutput.getCount();
|
||||||
if(countFromPreExecute > reportFormat.getMaxRows())
|
if(countFromPreExecute > reportFormat.getMaxRows())
|
@ -23,20 +23,22 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
|
|||||||
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Interface for various report formats to implement.
|
** Interface for various export formats to implement.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public interface ReportStreamerInterface
|
public interface ExportStreamerInterface
|
||||||
{
|
{
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Called once, before any rows are available. Meant to write a header, for example.
|
** Called once, before any rows are available. Meant to write a header, for example.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
void start(ReportInput reportInput, List<QFieldMetaData> fields) throws QReportingException;
|
void start(ExportInput exportInput, List<QFieldMetaData> fields) throws QReportingException;
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Called as records flow into the pipe.
|
** Called as records flow into the pipe.
|
||||||
@ -48,4 +50,21 @@ public interface ReportStreamerInterface
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
void finish() throws QReportingException;
|
void finish() throws QReportingException;
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
default void setDisplayFormats(Map<String, String> displayFormats)
|
||||||
|
{
|
||||||
|
// noop in base class
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
default void addTotalsRow(QRecord record) throws QReportingException
|
||||||
|
{
|
||||||
|
RecordPipe recordPipe = new RecordPipe();
|
||||||
|
recordPipe.addRecord(record);
|
||||||
|
takeRecordsFromPipe(recordPipe);
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,273 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.MathContext;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
||||||
|
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class FormulaInterpreter
|
||||||
|
{
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static Serializable interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula) throws QFormulaException
|
||||||
|
{
|
||||||
|
List<Serializable> results = interpretFormula(variableInterpreter, formula, new AtomicInteger(0));
|
||||||
|
if(results.size() == 1)
|
||||||
|
{
|
||||||
|
return (results.get(0));
|
||||||
|
}
|
||||||
|
else if(results.isEmpty())
|
||||||
|
{
|
||||||
|
throw (new QFormulaException("No results from formula"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw (new QFormulaException("More than 1 result from formula"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static List<Serializable> interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula, AtomicInteger i) throws QFormulaException
|
||||||
|
{
|
||||||
|
StringBuilder functionName = new StringBuilder();
|
||||||
|
List<Serializable> result = new ArrayList<>();
|
||||||
|
|
||||||
|
char previousChar = 0;
|
||||||
|
while(i.get() < formula.length())
|
||||||
|
{
|
||||||
|
if(i.get() > 0)
|
||||||
|
{
|
||||||
|
previousChar = formula.charAt(i.get() - 1);
|
||||||
|
}
|
||||||
|
char c = formula.charAt(i.getAndIncrement());
|
||||||
|
if(c == '(' && i.get() < formula.length() - 1)
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// open paren means: go into a sub-parse - to get a list of arguments for the current function //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
List<Serializable> args = interpretFormula(variableInterpreter, formula, i);
|
||||||
|
Serializable evaluate = evaluate(functionName.toString(), args, variableInterpreter);
|
||||||
|
result.add(evaluate);
|
||||||
|
}
|
||||||
|
else if(c == ')')
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// close paren means: end this sub-parse. evaluate the current function, add it to the result list, and return the result list. //
|
||||||
|
// unless we just closed a paren. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(previousChar != ')')
|
||||||
|
{
|
||||||
|
Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter);
|
||||||
|
result.add(evaluate);
|
||||||
|
}
|
||||||
|
return (result);
|
||||||
|
}
|
||||||
|
else if(c == ',')
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
// comma means: evaluate the current thing; add it to the result list //
|
||||||
|
// unless we just closed a paren. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
if(previousChar != ')')
|
||||||
|
{
|
||||||
|
Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter);
|
||||||
|
result.add(evaluate);
|
||||||
|
}
|
||||||
|
functionName = new StringBuilder();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
// else, we add this char to the current name //
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
functionName.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if we haven't found a result yet, assume we have just a literal, not a function call, and evaluate as such //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(result.isEmpty())
|
||||||
|
{
|
||||||
|
if(!functionName.isEmpty())
|
||||||
|
{
|
||||||
|
Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter);
|
||||||
|
result.add(evaluate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static Serializable evaluate(String functionName, List<Serializable> args, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
|
||||||
|
{
|
||||||
|
// System.out.format("== Evaluating [%s](%s) ==\n", functionName, args);
|
||||||
|
switch(functionName)
|
||||||
|
{
|
||||||
|
case "ADD":
|
||||||
|
{
|
||||||
|
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||||
|
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).add(numbers.get(1)));
|
||||||
|
}
|
||||||
|
case "MINUS":
|
||||||
|
{
|
||||||
|
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||||
|
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).subtract(numbers.get(1)));
|
||||||
|
}
|
||||||
|
case "MULTIPLY":
|
||||||
|
{
|
||||||
|
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||||
|
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).multiply(numbers.get(1)));
|
||||||
|
}
|
||||||
|
case "DIVIDE":
|
||||||
|
{
|
||||||
|
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||||
|
if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
case "DIVIDE_SCALE":
|
||||||
|
{
|
||||||
|
List<BigDecimal> numbers = getNumberArgumentList(args, 3, variableInterpreter);
|
||||||
|
if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
case "ROUND":
|
||||||
|
{
|
||||||
|
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||||
|
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue())));
|
||||||
|
}
|
||||||
|
case "SCALE":
|
||||||
|
{
|
||||||
|
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
|
||||||
|
return nullIfAnyNullArgsElse(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP));
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if there aren't arguments, then we can try to evaluate the thing not as a function //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(CollectionUtils.nullSafeIsEmpty(args))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (ValueUtils.getValueAsBigDecimal(functionName));
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (variableInterpreter.interpret(functionName));
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw (new QFormulaException("Unable to evaluate unrecognized expression: " + functionName + ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static Serializable nullIfAnyNullArgsElse(List<BigDecimal> numbers, Supplier<BigDecimal> supplier)
|
||||||
|
{
|
||||||
|
if(numbers.stream().anyMatch(Objects::isNull))
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
return supplier.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static List<BigDecimal> getNumberArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
|
||||||
|
{
|
||||||
|
if(howMany != null)
|
||||||
|
{
|
||||||
|
if(!howMany.equals(originalArgs.size()))
|
||||||
|
{
|
||||||
|
throw (new QFormulaException("Wrong number of arguments (required: " + howMany + ", received: " + originalArgs.size() + ")"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BigDecimal> rs = new ArrayList<>();
|
||||||
|
for(Serializable originalArg : originalArgs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg));
|
||||||
|
rs.add(ValueUtils.getValueAsBigDecimal(interpretedArg));
|
||||||
|
}
|
||||||
|
catch(QValueException e)
|
||||||
|
{
|
||||||
|
throw (new QFormulaException("Could not process [" + originalArg + "] as a number"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (rs);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,510 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||||
|
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.Pair;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Action to generate a report!!
|
||||||
|
*******************************************************************************/
|
||||||
|
public class GenerateReportAction
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
// viewName > PivotKey > fieldName > Aggregates //
|
||||||
|
//////////////////////////////////////////////////
|
||||||
|
Map<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> pivotAggregates = new HashMap<>();
|
||||||
|
|
||||||
|
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void execute(ReportInput reportInput) throws QException
|
||||||
|
{
|
||||||
|
gatherData(reportInput);
|
||||||
|
output(reportInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void gatherData(ReportInput reportInput) throws QException
|
||||||
|
{
|
||||||
|
QReportMetaData report = reportInput.getInstance().getReport(reportInput.getReportName());
|
||||||
|
QQueryFilter queryFilter = report.getQueryFilter();
|
||||||
|
|
||||||
|
setInputValuesInQueryFilter(reportInput, queryFilter);
|
||||||
|
|
||||||
|
RecordPipe recordPipe = new RecordPipe();
|
||||||
|
new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
|
||||||
|
{
|
||||||
|
QueryInput queryInput = new QueryInput(reportInput.getInstance());
|
||||||
|
queryInput.setSession(reportInput.getSession());
|
||||||
|
queryInput.setRecordPipe(recordPipe);
|
||||||
|
queryInput.setTableName(report.getSourceTable());
|
||||||
|
queryInput.setFilter(queryFilter);
|
||||||
|
return (new QueryAction().execute(queryInput));
|
||||||
|
}, () -> consumeRecords(report, reportInput, recordPipe));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter)
|
||||||
|
{
|
||||||
|
if(queryFilter == null || queryFilter.getCriteria() == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||||
|
variableInterpreter.addValueMap("input", reportInput.getInputValues());
|
||||||
|
for(QFilterCriteria criterion : queryFilter.getCriteria())
|
||||||
|
{
|
||||||
|
if(criterion.getValues() != null)
|
||||||
|
{
|
||||||
|
List<Serializable> newValues = new ArrayList<>();
|
||||||
|
|
||||||
|
for(Serializable value : criterion.getValues())
|
||||||
|
{
|
||||||
|
String valueAsString = ValueUtils.getValueAsString(value);
|
||||||
|
Serializable interpretedValue = variableInterpreter.interpret(valueAsString);
|
||||||
|
newValues.add(interpretedValue);
|
||||||
|
}
|
||||||
|
criterion.setValues(newValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private Integer consumeRecords(QReportMetaData report, ReportInput reportInput, RecordPipe recordPipe)
|
||||||
|
{
|
||||||
|
// todo - stream to output if report has a simple type output
|
||||||
|
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// do aggregates for pivots //
|
||||||
|
//////////////////////////////
|
||||||
|
QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable());
|
||||||
|
report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).forEach((view) ->
|
||||||
|
{
|
||||||
|
doPivotAggregates(view, table, records);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (records.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void doPivotAggregates(QReportView view, QTableMetaData table, List<QRecord> records)
|
||||||
|
{
|
||||||
|
Map<PivotKey, Map<String, AggregatesInterface<?>>> viewAggregates = pivotAggregates.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
|
||||||
|
|
||||||
|
for(QRecord record : records)
|
||||||
|
{
|
||||||
|
PivotKey key = new PivotKey();
|
||||||
|
for(String pivotField : view.getPivotFields())
|
||||||
|
{
|
||||||
|
key.add(pivotField, record.getValue(pivotField));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
|
||||||
|
|
||||||
|
for(QFieldMetaData field : table.getFields().values())
|
||||||
|
{
|
||||||
|
if(field.getType().equals(QFieldType.INTEGER))
|
||||||
|
{
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
AggregatesInterface<Integer> fieldAggregates = (AggregatesInterface<Integer>) keyAggregates.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
|
||||||
|
fieldAggregates.add(record.getValueInteger(field.getName()));
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
AggregatesInterface<Integer> fieldTotalAggregates = (AggregatesInterface<Integer>) totalAggregates.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
|
||||||
|
fieldTotalAggregates.add(record.getValueInteger(field.getName()));
|
||||||
|
}
|
||||||
|
else if(field.getType().equals(QFieldType.DECIMAL))
|
||||||
|
{
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
AggregatesInterface<BigDecimal> fieldAggregates = (AggregatesInterface<BigDecimal>) keyAggregates.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
|
||||||
|
fieldAggregates.add(record.getValueBigDecimal(field.getName()));
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
AggregatesInterface<BigDecimal> fieldTotalAggregates = (AggregatesInterface<BigDecimal>) totalAggregates.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
|
||||||
|
fieldTotalAggregates.add(record.getValueBigDecimal(field.getName()));
|
||||||
|
}
|
||||||
|
// todo - more types (dates, at least?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void output(ReportInput reportInput) throws QReportingException, QFormulaException
|
||||||
|
{
|
||||||
|
QReportMetaData report = reportInput.getInstance().getReport(reportInput.getReportName());
|
||||||
|
QTableMetaData table = reportInput.getInstance().getTable(report.getSourceTable());
|
||||||
|
|
||||||
|
List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.PIVOT)).toList();
|
||||||
|
for(QReportView view : reportViews)
|
||||||
|
{
|
||||||
|
PivotOutput pivotOutput = outputPivot(reportInput, view, table);
|
||||||
|
ReportFormat reportFormat = reportInput.getReportFormat();
|
||||||
|
|
||||||
|
ExportInput exportInput = new ExportInput(reportInput.getInstance());
|
||||||
|
exportInput.setSession(reportInput.getSession());
|
||||||
|
exportInput.setReportFormat(reportFormat);
|
||||||
|
exportInput.setFilename(reportInput.getFilename());
|
||||||
|
exportInput.setTitleRow(pivotOutput.titleRow);
|
||||||
|
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
||||||
|
|
||||||
|
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
|
||||||
|
reportStreamer.setDisplayFormats(getDisplayFormatMap(view));
|
||||||
|
reportStreamer.start(exportInput, getFields(table, view));
|
||||||
|
|
||||||
|
RecordPipe recordPipe = new RecordPipe(); // todo - make it an unlimited pipe or something...
|
||||||
|
recordPipe.addRecords(pivotOutput.pivotRows);
|
||||||
|
reportStreamer.takeRecordsFromPipe(recordPipe);
|
||||||
|
|
||||||
|
if(pivotOutput.totalRow != null)
|
||||||
|
{
|
||||||
|
reportStreamer.addTotalsRow(pivotOutput.totalRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
reportStreamer.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private Map<String, String> getDisplayFormatMap(QReportView view)
|
||||||
|
{
|
||||||
|
return (view.getColumns().stream()
|
||||||
|
.filter(c -> c.getDisplayFormat() != null)
|
||||||
|
.collect(Collectors.toMap(QReportField::getName, QReportField::getDisplayFormat)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private List<QFieldMetaData> getFields(QTableMetaData table, QReportView view)
|
||||||
|
{
|
||||||
|
List<QFieldMetaData> fields = new ArrayList<>();
|
||||||
|
for(String pivotField : view.getPivotFields())
|
||||||
|
{
|
||||||
|
QFieldMetaData field = table.getField(pivotField);
|
||||||
|
fields.add(new QFieldMetaData(pivotField, field.getType()).withLabel(field.getLabel())); // todo do we need the type? if so need table as input here
|
||||||
|
}
|
||||||
|
for(QReportField column : view.getColumns())
|
||||||
|
{
|
||||||
|
fields.add(new QFieldMetaData().withName(column.getName()).withLabel(column.getLabel())); // todo do we need the type? if so need table as input here
|
||||||
|
}
|
||||||
|
return (fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private PivotOutput outputPivot(ReportInput reportInput, QReportView view, QTableMetaData table) throws QReportingException, QFormulaException
|
||||||
|
{
|
||||||
|
QValueFormatter valueFormatter = new QValueFormatter();
|
||||||
|
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||||
|
variableInterpreter.addValueMap("input", reportInput.getInputValues());
|
||||||
|
variableInterpreter.addValueMap("total", getPivotValuesForInterpreter(totalAggregates));
|
||||||
|
|
||||||
|
///////////
|
||||||
|
// title //
|
||||||
|
///////////
|
||||||
|
String title = null;
|
||||||
|
if(view.getTitleFields() != null && StringUtils.hasContent(view.getTitleFormat()))
|
||||||
|
{
|
||||||
|
List<String> titleValues = new ArrayList<>();
|
||||||
|
for(String titleField : view.getTitleFields())
|
||||||
|
{
|
||||||
|
titleValues.add(variableInterpreter.interpret(titleField));
|
||||||
|
}
|
||||||
|
|
||||||
|
title = valueFormatter.formatStringWithValues(view.getTitleFormat(), titleValues);
|
||||||
|
}
|
||||||
|
else if(StringUtils.hasContent(view.getTitleFormat()))
|
||||||
|
{
|
||||||
|
title = view.getTitleFormat();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(StringUtils.hasContent(title))
|
||||||
|
{
|
||||||
|
System.out.println(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////
|
||||||
|
// headers //
|
||||||
|
/////////////
|
||||||
|
for(String field : view.getPivotFields())
|
||||||
|
{
|
||||||
|
System.out.printf("%-15s", table.getField(field).getLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
for(QReportField column : view.getColumns())
|
||||||
|
{
|
||||||
|
System.out.printf("%25s", column.getLabel());
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
///////////////////////
|
||||||
|
// create pivot rows //
|
||||||
|
///////////////////////
|
||||||
|
List<QRecord> pivotRows = new ArrayList<>();
|
||||||
|
for(Map.Entry<PivotKey, Map<String, AggregatesInterface<?>>> entry : pivotAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
|
||||||
|
{
|
||||||
|
PivotKey pivotKey = entry.getKey();
|
||||||
|
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
|
||||||
|
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates));
|
||||||
|
|
||||||
|
QRecord pivotRow = new QRecord();
|
||||||
|
pivotRows.add(pivotRow);
|
||||||
|
for(Pair<String, Serializable> key : pivotKey.getKeys())
|
||||||
|
{
|
||||||
|
pivotRow.setValue(key.getA(), key.getB());
|
||||||
|
}
|
||||||
|
|
||||||
|
for(QReportField column : view.getColumns())
|
||||||
|
{
|
||||||
|
Serializable serializable = getValueForColumn(variableInterpreter, column);
|
||||||
|
pivotRow.setValue(column.getName(), serializable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////
|
||||||
|
// sort the pivot rows //
|
||||||
|
/////////////////////////
|
||||||
|
if(CollectionUtils.nullSafeHasContents(view.getOrderByFields()))
|
||||||
|
{
|
||||||
|
pivotRows.sort((o1, o2) ->
|
||||||
|
{
|
||||||
|
return pivotRowComparator(view, o1, o2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// print the rows (just debugging i think) //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
for(QRecord pivotRow : pivotRows)
|
||||||
|
{
|
||||||
|
for(String pivotField : view.getPivotFields())
|
||||||
|
{
|
||||||
|
System.out.printf("%-15s", pivotRow.getValue(pivotField));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(QReportField column : view.getColumns())
|
||||||
|
{
|
||||||
|
Serializable serializable = pivotRow.getValue(column.getName());
|
||||||
|
String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable);
|
||||||
|
System.out.printf("%25s", formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////
|
||||||
|
// totals row //
|
||||||
|
////////////////
|
||||||
|
QRecord totalRow = null;
|
||||||
|
if(view.getTotalRow())
|
||||||
|
{
|
||||||
|
totalRow = new QRecord();
|
||||||
|
|
||||||
|
for(String pivotField : view.getPivotFields())
|
||||||
|
{
|
||||||
|
if(totalRow.getValues().isEmpty())
|
||||||
|
{
|
||||||
|
totalRow.setValue(pivotField, "Totals");
|
||||||
|
System.out.printf("%-15s", "Totals");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System.out.printf("%-15s", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates));
|
||||||
|
for(QReportField column : view.getColumns())
|
||||||
|
{
|
||||||
|
Serializable serializable = getValueForColumn(variableInterpreter, column);
|
||||||
|
totalRow.setValue(column.getName(), serializable);
|
||||||
|
|
||||||
|
String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable);
|
||||||
|
System.out.printf("%25s", formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (new PivotOutput(pivotRows, title, totalRow));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private Serializable getValueForColumn(QMetaDataVariableInterpreter variableInterpreter, QReportField column) throws QFormulaException
|
||||||
|
{
|
||||||
|
String formula = column.getFormula();
|
||||||
|
Serializable serializable = variableInterpreter.interpretForObject(formula);
|
||||||
|
if(formula.startsWith("=") && formula.length() > 1)
|
||||||
|
{
|
||||||
|
// serializable = interpretFormula(variableInterpreter, formula);
|
||||||
|
serializable = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1));
|
||||||
|
}
|
||||||
|
return serializable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||||
|
private int pivotRowComparator(QReportView view, QRecord o1, QRecord o2)
|
||||||
|
{
|
||||||
|
if(o1 == o2)
|
||||||
|
{
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(QFilterOrderBy orderByField : view.getOrderByFields())
|
||||||
|
{
|
||||||
|
Comparable c1 = (Comparable) o1.getValue(orderByField.getFieldName());
|
||||||
|
Comparable c2 = (Comparable) o2.getValue(orderByField.getFieldName());
|
||||||
|
|
||||||
|
if(c1 == null && c2 == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(c1 == null)
|
||||||
|
{
|
||||||
|
return (orderByField.getIsAscending() ? -1 : 1);
|
||||||
|
}
|
||||||
|
if(c2 == null)
|
||||||
|
{
|
||||||
|
return (orderByField.getIsAscending() ? 1 : -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int comp = orderByField.getIsAscending() ? c1.compareTo(c2) : c2.compareTo(c1);
|
||||||
|
if(comp != 0)
|
||||||
|
{
|
||||||
|
return (comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private Map<String, Serializable> getPivotValuesForInterpreter(Map<String, AggregatesInterface<?>> fieldAggregates)
|
||||||
|
{
|
||||||
|
Map<String, Serializable> pivotValuesForInterpreter = new HashMap<>();
|
||||||
|
for(Map.Entry<String, AggregatesInterface<?>> subEntry : fieldAggregates.entrySet())
|
||||||
|
{
|
||||||
|
String fieldName = subEntry.getKey();
|
||||||
|
AggregatesInterface<?> aggregates = subEntry.getValue();
|
||||||
|
pivotValuesForInterpreter.put("sum." + fieldName, aggregates.getSum());
|
||||||
|
pivotValuesForInterpreter.put("count." + fieldName, aggregates.getCount());
|
||||||
|
// todo min, max, avg
|
||||||
|
}
|
||||||
|
return pivotValuesForInterpreter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** record to serve as tuple/multi-value output of outputPivot method.
|
||||||
|
*******************************************************************************/
|
||||||
|
private record PivotOutput(List<QRecord> pivotRows, String titleRow, QRecord totalRow)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Report streamer implementation that just builds up a STATIC list of lists of strings.
|
||||||
|
** Meant only for use in unit tests at this time... would need refactored for
|
||||||
|
** multi-thread/multi-use if wanted for real usage.
|
||||||
|
*******************************************************************************/
|
||||||
|
public class ListOfMapsExportStreamer implements ExportStreamerInterface
|
||||||
|
{
|
||||||
|
private static final Logger LOG = LogManager.getLogger(ListOfMapsExportStreamer.class);
|
||||||
|
|
||||||
|
private ExportInput exportInput;
|
||||||
|
private List<QFieldMetaData> fields;
|
||||||
|
|
||||||
|
private static List<Map<String, String>> list = new ArrayList<>();
|
||||||
|
private static List<String> headers = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public ListOfMapsExportStreamer()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for list
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static List<Map<String, String>> getList()
|
||||||
|
{
|
||||||
|
return (list);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void start(ExportInput exportInput, List<QFieldMetaData> fields) throws QReportingException
|
||||||
|
{
|
||||||
|
this.exportInput = exportInput;
|
||||||
|
this.fields = fields;
|
||||||
|
|
||||||
|
headers = new ArrayList<>();
|
||||||
|
for(QFieldMetaData field : fields)
|
||||||
|
{
|
||||||
|
headers.add(field.getLabel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException
|
||||||
|
{
|
||||||
|
List<QRecord> qRecords = recordPipe.consumeAvailableRecords();
|
||||||
|
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||||
|
|
||||||
|
for(QRecord qRecord : qRecords)
|
||||||
|
{
|
||||||
|
addRecord(qRecord);
|
||||||
|
}
|
||||||
|
return (qRecords.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void addRecord(QRecord qRecord)
|
||||||
|
{
|
||||||
|
Map<String, String> row = new LinkedHashMap<>();
|
||||||
|
list.add(row);
|
||||||
|
for(int i = 0; i < fields.size(); i++)
|
||||||
|
{
|
||||||
|
row.put(headers.get(i), qRecord.getValueString(fields.get(i).getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void addTotalsRow(QRecord record) throws QReportingException
|
||||||
|
{
|
||||||
|
addRecord(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void finish()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.Pair;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class PivotKey
|
||||||
|
{
|
||||||
|
private List<Pair<String, Serializable>> keys = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public PivotKey()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "PivotKey{keys=" + keys + '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void add(String field, Serializable value)
|
||||||
|
{
|
||||||
|
keys.add(new Pair<>(field, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for keys
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<Pair<String, Serializable>> getKeys()
|
||||||
|
{
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if(this == o)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(o == null || getClass() != o.getClass())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PivotKey pivotKey = (PivotKey) o;
|
||||||
|
return Objects.equals(keys, pivotKey.keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return Objects.hash(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
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.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
@ -47,6 +48,26 @@ public class QValueFormatter
|
|||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public String formatValue(QFieldMetaData field, Serializable value)
|
public String formatValue(QFieldMetaData field, Serializable value)
|
||||||
|
{
|
||||||
|
return (formatValue(field.getDisplayFormat(), field.getName(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String formatValue(String displayFormat, Serializable value)
|
||||||
|
{
|
||||||
|
return (formatValue(displayFormat, "", value));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private String formatValue(String displayFormat, String fieldName, Serializable value)
|
||||||
{
|
{
|
||||||
//////////////////////////////////
|
//////////////////////////////////
|
||||||
// null values get null results //
|
// null values get null results //
|
||||||
@ -59,11 +80,11 @@ public class QValueFormatter
|
|||||||
////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////
|
||||||
// if the field has a display format, try to apply it //
|
// if the field has a display format, try to apply it //
|
||||||
////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////
|
||||||
if(StringUtils.hasContent(field.getDisplayFormat()))
|
if(StringUtils.hasContent(displayFormat))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return (field.getDisplayFormat().formatted(value));
|
return (displayFormat.formatted(value));
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
@ -72,24 +93,24 @@ public class QValueFormatter
|
|||||||
// todo - revisit if we actually want this - or - if you should get an error if you mis-configure your table this way (ideally during validation!)
|
// todo - revisit if we actually want this - or - if you should get an error if you mis-configure your table this way (ideally during validation!)
|
||||||
if(e.getMessage().equals("f != java.lang.Integer"))
|
if(e.getMessage().equals("f != java.lang.Integer"))
|
||||||
{
|
{
|
||||||
return formatValue(field, ValueUtils.getValueAsBigDecimal(value));
|
return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value));
|
||||||
}
|
}
|
||||||
else if(e.getMessage().equals("f != java.lang.String"))
|
else if(e.getMessage().equals("f != java.lang.String"))
|
||||||
{
|
{
|
||||||
return formatValue(field, ValueUtils.getValueAsBigDecimal(value));
|
return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value));
|
||||||
}
|
}
|
||||||
else if(e.getMessage().equals("d != java.math.BigDecimal"))
|
else if(e.getMessage().equals("d != java.math.BigDecimal"))
|
||||||
{
|
{
|
||||||
return formatValue(field, ValueUtils.getValueAsInteger(value));
|
return formatValue(displayFormat, ValueUtils.getValueAsInteger(value));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LOG.warn("Error formatting value [" + value + "] for field [" + field.getName() + "] with format [" + field.getDisplayFormat() + "]: " + e.getMessage());
|
LOG.warn("Error formatting value [" + value + "] for field [" + fieldName + "] with format [" + displayFormat + "]: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(Exception e2)
|
catch(Exception e2)
|
||||||
{
|
{
|
||||||
LOG.warn("Caught secondary exception trying to convert type on field [" + field.getName() + "] for formatting", e);
|
LOG.warn("Caught secondary exception trying to convert type on field [" + fieldName + "] for formatting", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,11 +138,7 @@ public class QValueFormatter
|
|||||||
///////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
List<Serializable> values = table.getRecordLabelFields().stream()
|
return formatStringWithFields(table.getRecordLabelFormat(), table.getRecordLabelFields(), record.getValues());
|
||||||
.map(record::getValue)
|
|
||||||
.map(v -> v == null ? "" : v)
|
|
||||||
.toList();
|
|
||||||
return (table.getRecordLabelFormat().formatted(values.toArray()));
|
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
@ -131,6 +148,33 @@ public class QValueFormatter
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String formatStringWithFields(String formatString, List<String> formatFields, Map<String, Serializable> valueMap)
|
||||||
|
{
|
||||||
|
List<Serializable> values = formatFields.stream()
|
||||||
|
.map(valueMap::get)
|
||||||
|
.map(v -> v == null ? "" : v)
|
||||||
|
.toList();
|
||||||
|
return (formatString.formatted(values.toArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String formatStringWithValues(String formatString, List<String> formatValues)
|
||||||
|
{
|
||||||
|
List<String> values = formatValues.stream()
|
||||||
|
.map(v -> v == null ? "" : v)
|
||||||
|
.toList();
|
||||||
|
return (formatString.formatted(values.toArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Deal with non-happy-path cases for making a record label.
|
** Deal with non-happy-path cases for making a record label.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.exceptions;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
* Exception thrown while generating reports
|
||||||
|
*
|
||||||
|
*******************************************************************************/
|
||||||
|
public class QFormulaException extends QException
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor of message
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QFormulaException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor of message & cause
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QFormulaException(String message, Throwable cause)
|
||||||
|
{
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
@ -22,11 +22,14 @@
|
|||||||
package com.kingsrook.qqq.backend.core.instances;
|
package com.kingsrook.qqq.backend.core.instances;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
import io.github.cdimascio.dotenv.Dotenv;
|
import io.github.cdimascio.dotenv.Dotenv;
|
||||||
import io.github.cdimascio.dotenv.DotenvEntry;
|
import io.github.cdimascio.dotenv.DotenvEntry;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@ -49,6 +52,7 @@ public class QMetaDataVariableInterpreter
|
|||||||
private static final Logger LOG = LogManager.getLogger(QMetaDataVariableInterpreter.class);
|
private static final Logger LOG = LogManager.getLogger(QMetaDataVariableInterpreter.class);
|
||||||
|
|
||||||
private Map<String, String> environmentOverrides;
|
private Map<String, String> environmentOverrides;
|
||||||
|
private Map<String, Map<String, Serializable>> valueMaps;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -121,6 +125,18 @@ public class QMetaDataVariableInterpreter
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Interpret a value string, which may be a variable, into its run-time value -
|
||||||
|
** always as a String.
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String interpret(String value)
|
||||||
|
{
|
||||||
|
return (ValueUtils.getValueAsString(interpretForObject(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Interpret a value string, which may be a variable, into its run-time value.
|
** Interpret a value string, which may be a variable, into its run-time value.
|
||||||
**
|
**
|
||||||
@ -131,7 +147,7 @@ public class QMetaDataVariableInterpreter
|
|||||||
** - used if you really want to get back the literal value, ${env.X}, for example.
|
** - used if you really want to get back the literal value, ${env.X}, for example.
|
||||||
** Else the output is the input.
|
** Else the output is the input.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public String interpret(String value)
|
public Serializable interpretForObject(String value)
|
||||||
{
|
{
|
||||||
if(value == null)
|
if(value == null)
|
||||||
{
|
{
|
||||||
@ -142,23 +158,39 @@ public class QMetaDataVariableInterpreter
|
|||||||
if(value.startsWith(envPrefix) && value.endsWith("}"))
|
if(value.startsWith(envPrefix) && value.endsWith("}"))
|
||||||
{
|
{
|
||||||
String envVarName = value.substring(envPrefix.length()).replaceFirst("}$", "");
|
String envVarName = value.substring(envPrefix.length()).replaceFirst("}$", "");
|
||||||
String envValue = getEnvironmentVariable(envVarName);
|
return (getEnvironmentVariable(envVarName));
|
||||||
return (envValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String propPrefix = "${prop.";
|
String propPrefix = "${prop.";
|
||||||
if(value.startsWith(propPrefix) && value.endsWith("}"))
|
if(value.startsWith(propPrefix) && value.endsWith("}"))
|
||||||
{
|
{
|
||||||
String propertyName = value.substring(propPrefix.length()).replaceFirst("}$", "");
|
String propertyName = value.substring(propPrefix.length()).replaceFirst("}$", "");
|
||||||
String propertyValue = System.getProperty(propertyName);
|
return (System.getProperty(propertyName));
|
||||||
return (propertyValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String literalPrefix = "${literal.";
|
String literalPrefix = "${literal.";
|
||||||
if(value.startsWith(literalPrefix) && value.endsWith("}"))
|
if(value.startsWith(literalPrefix) && value.endsWith("}"))
|
||||||
{
|
{
|
||||||
String literalValue = value.substring(literalPrefix.length()).replaceFirst("}$", "");
|
return (value.substring(literalPrefix.length()).replaceFirst("}$", ""));
|
||||||
return (literalValue);
|
}
|
||||||
|
|
||||||
|
if(valueMaps != null)
|
||||||
|
{
|
||||||
|
for(Map.Entry<String, Map<String, Serializable>> entry : valueMaps.entrySet())
|
||||||
|
{
|
||||||
|
String name = entry.getKey();
|
||||||
|
Map<String, Serializable> valueMap = entry.getValue();
|
||||||
|
|
||||||
|
String prefix = "${" + name + ".";
|
||||||
|
if(value.startsWith(prefix) && value.endsWith("}"))
|
||||||
|
{
|
||||||
|
String lookupName = value.substring(prefix.length()).replaceFirst("}$", "");
|
||||||
|
if(valueMap != null && valueMap.containsKey(lookupName))
|
||||||
|
{
|
||||||
|
return (valueMap.get(lookupName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (value);
|
return (value);
|
||||||
@ -190,4 +222,19 @@ public class QMetaDataVariableInterpreter
|
|||||||
|
|
||||||
return System.getenv(key);
|
return System.getenv(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void addValueMap(String name, Map<String, Serializable> values)
|
||||||
|
{
|
||||||
|
if(valueMaps == null)
|
||||||
|
{
|
||||||
|
valueMaps = new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
valueMaps.put(name, values);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.model.actions.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Input for an Export action
|
||||||
|
*******************************************************************************/
|
||||||
|
public class ExportInput extends AbstractTableActionInput
|
||||||
|
{
|
||||||
|
private QQueryFilter queryFilter;
|
||||||
|
private Integer limit;
|
||||||
|
private List<String> fieldNames;
|
||||||
|
|
||||||
|
private String filename;
|
||||||
|
private ReportFormat reportFormat;
|
||||||
|
private OutputStream reportOutputStream;
|
||||||
|
private String titleRow;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public ExportInput()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public ExportInput(QInstance instance)
|
||||||
|
{
|
||||||
|
super(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public ExportInput(QInstance instance, QSession session)
|
||||||
|
{
|
||||||
|
super(instance);
|
||||||
|
setSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for queryFilter
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QQueryFilter getQueryFilter()
|
||||||
|
{
|
||||||
|
return queryFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for queryFilter
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setQueryFilter(QQueryFilter queryFilter)
|
||||||
|
{
|
||||||
|
this.queryFilter = queryFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for limit
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Integer getLimit()
|
||||||
|
{
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for limit
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setLimit(Integer limit)
|
||||||
|
{
|
||||||
|
this.limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for fieldNames
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<String> getFieldNames()
|
||||||
|
{
|
||||||
|
return fieldNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for fieldNames
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setFieldNames(List<String> fieldNames)
|
||||||
|
{
|
||||||
|
this.fieldNames = fieldNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for filename
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getFilename()
|
||||||
|
{
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for filename
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setFilename(String filename)
|
||||||
|
{
|
||||||
|
this.filename = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for reportFormat
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public ReportFormat getReportFormat()
|
||||||
|
{
|
||||||
|
return reportFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for reportFormat
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setReportFormat(ReportFormat reportFormat)
|
||||||
|
{
|
||||||
|
this.reportFormat = reportFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for reportOutputStream
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public OutputStream getReportOutputStream()
|
||||||
|
{
|
||||||
|
return reportOutputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for reportOutputStream
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setReportOutputStream(OutputStream reportOutputStream)
|
||||||
|
{
|
||||||
|
this.reportOutputStream = reportOutputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getTitleRow()
|
||||||
|
{
|
||||||
|
return titleRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setTitleRow(String titleRow)
|
||||||
|
{
|
||||||
|
this.titleRow = titleRow;
|
||||||
|
}
|
||||||
|
}
|
@ -26,9 +26,9 @@ import java.io.Serializable;
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Output for a Report action
|
** Output for an Export action
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class ReportOutput implements Serializable
|
public class ExportOutput implements Serializable
|
||||||
{
|
{
|
||||||
public long recordCount;
|
public long recordCount;
|
||||||
|
|
@ -24,9 +24,10 @@ package com.kingsrook.qqq.backend.core.model.actions.reporting;
|
|||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import com.kingsrook.qqq.backend.core.actions.reporting.CsvReportStreamer;
|
import com.kingsrook.qqq.backend.core.actions.reporting.CsvExportStreamer;
|
||||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExcelReportStreamer;
|
import com.kingsrook.qqq.backend.core.actions.reporting.ExcelExportStreamer;
|
||||||
import com.kingsrook.qqq.backend.core.actions.reporting.ReportStreamerInterface;
|
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.reporting.ListOfMapsExportStreamer;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
import org.dhatim.fastexcel.Worksheet;
|
import org.dhatim.fastexcel.Worksheet;
|
||||||
@ -37,22 +38,23 @@ import org.dhatim.fastexcel.Worksheet;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public enum ReportFormat
|
public enum ReportFormat
|
||||||
{
|
{
|
||||||
XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelReportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
||||||
CSV(null, null, CsvReportStreamer::new, "text/csv");
|
CSV(null, null, CsvExportStreamer::new, "text/csv"),
|
||||||
|
LIST_OF_MAPS(null, null, ListOfMapsExportStreamer::new, null);
|
||||||
|
|
||||||
|
|
||||||
private final Integer maxRows;
|
private final Integer maxRows;
|
||||||
private final Integer maxCols;
|
private final Integer maxCols;
|
||||||
private final String mimeType;
|
private final String mimeType;
|
||||||
|
|
||||||
private final Supplier<? extends ReportStreamerInterface> streamerConstructor;
|
private final Supplier<? extends ExportStreamerInterface> streamerConstructor;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
ReportFormat(Integer maxRows, Integer maxCols, Supplier<? extends ReportStreamerInterface> streamerConstructor, String mimeType)
|
ReportFormat(Integer maxRows, Integer maxCols, Supplier<? extends ExportStreamerInterface> streamerConstructor, String mimeType)
|
||||||
{
|
{
|
||||||
this.maxRows = maxRows;
|
this.maxRows = maxRows;
|
||||||
this.maxCols = maxCols;
|
this.maxCols = maxCols;
|
||||||
@ -94,6 +96,7 @@ public enum ReportFormat
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for maxCols
|
** Getter for maxCols
|
||||||
**
|
**
|
||||||
@ -119,7 +122,7 @@ public enum ReportFormat
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public ReportStreamerInterface newReportStreamer()
|
public ExportStreamerInterface newReportStreamer()
|
||||||
{
|
{
|
||||||
return (streamerConstructor.get());
|
return (streamerConstructor.get());
|
||||||
}
|
}
|
||||||
|
@ -23,21 +23,20 @@ package com.kingsrook.qqq.backend.core.model.actions.reporting;
|
|||||||
|
|
||||||
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.List;
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Input for a Report action
|
** Input for an Export action
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class ReportInput extends AbstractTableActionInput
|
public class ReportInput extends AbstractTableActionInput
|
||||||
{
|
{
|
||||||
private QQueryFilter queryFilter;
|
private String reportName;
|
||||||
private Integer limit;
|
private Map<String, Serializable> inputValues;
|
||||||
private List<String> fieldNames;
|
|
||||||
|
|
||||||
private String filename;
|
private String filename;
|
||||||
private ReportFormat reportFormat;
|
private ReportFormat reportFormat;
|
||||||
@ -76,67 +75,45 @@ public class ReportInput extends AbstractTableActionInput
|
|||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for queryFilter
|
** Getter for reportName
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public QQueryFilter getQueryFilter()
|
public String getReportName()
|
||||||
{
|
{
|
||||||
return queryFilter;
|
return reportName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Setter for queryFilter
|
** Setter for reportName
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void setQueryFilter(QQueryFilter queryFilter)
|
public void setReportName(String reportName)
|
||||||
{
|
{
|
||||||
this.queryFilter = queryFilter;
|
this.reportName = reportName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for limit
|
** Getter for inputValues
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public Integer getLimit()
|
public Map<String, Serializable> getInputValues()
|
||||||
{
|
{
|
||||||
return limit;
|
return inputValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Setter for limit
|
** Setter for inputValues
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public void setLimit(Integer limit)
|
public void setInputValues(Map<String, Serializable> inputValues)
|
||||||
{
|
{
|
||||||
this.limit = limit;
|
this.inputValues = inputValues;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Getter for fieldNames
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public List<String> getFieldNames()
|
|
||||||
{
|
|
||||||
return fieldNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Setter for fieldNames
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
public void setFieldNames(List<String> fieldNames)
|
|
||||||
{
|
|
||||||
this.fieldNames = fieldNames;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,6 +36,37 @@ public class QFilterOrderBy implements Serializable
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Default no-arg constructor
|
||||||
|
*******************************************************************************/
|
||||||
|
public QFilterOrderBy()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor that sets field name, but leaves default for isAscending (true)
|
||||||
|
*******************************************************************************/
|
||||||
|
public QFilterOrderBy(String fieldName)
|
||||||
|
{
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Constructor that takes field name and isAscending.
|
||||||
|
*******************************************************************************/
|
||||||
|
public QFilterOrderBy(String fieldName, boolean isAscending)
|
||||||
|
{
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
this.isAscending = isAscending;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Getter for fieldName
|
** Getter for fieldName
|
||||||
**
|
**
|
||||||
|
@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
|
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
@ -64,6 +65,7 @@ public class QInstance
|
|||||||
private Map<String, QPossibleValueSource> possibleValueSources = new LinkedHashMap<>();
|
private Map<String, QPossibleValueSource> possibleValueSources = new LinkedHashMap<>();
|
||||||
private Map<String, QProcessMetaData> processes = new LinkedHashMap<>();
|
private Map<String, QProcessMetaData> processes = new LinkedHashMap<>();
|
||||||
private Map<String, QAppMetaData> apps = new LinkedHashMap<>();
|
private Map<String, QAppMetaData> apps = new LinkedHashMap<>();
|
||||||
|
private Map<String, QReportMetaData> reports = new LinkedHashMap<>();
|
||||||
|
|
||||||
private Map<String, QWidgetMetaDataInterface> widgets = new LinkedHashMap<>();
|
private Map<String, QWidgetMetaDataInterface> widgets = new LinkedHashMap<>();
|
||||||
|
|
||||||
@ -432,6 +434,66 @@ public class QInstance
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void addReport(QReportMetaData report)
|
||||||
|
{
|
||||||
|
this.addReport(report.getName(), report);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void addReport(String name, QReportMetaData report)
|
||||||
|
{
|
||||||
|
if(!StringUtils.hasContent(name))
|
||||||
|
{
|
||||||
|
throw (new IllegalArgumentException("Attempted to add an report without a name."));
|
||||||
|
}
|
||||||
|
if(this.reports.containsKey(name))
|
||||||
|
{
|
||||||
|
throw (new IllegalArgumentException("Attempted to add a second report with name: " + name));
|
||||||
|
}
|
||||||
|
this.reports.put(name, report);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportMetaData getReport(String name)
|
||||||
|
{
|
||||||
|
return (this.reports.get(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for reports
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Map<String, QReportMetaData> getReports()
|
||||||
|
{
|
||||||
|
return reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for reports
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setReports(Map<String, QReportMetaData> reports)
|
||||||
|
{
|
||||||
|
this.reports = reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -30,11 +30,49 @@ public interface DisplayFormat
|
|||||||
String DEFAULT = "%s";
|
String DEFAULT = "%s";
|
||||||
String STRING = "%s";
|
String STRING = "%s";
|
||||||
String COMMAS = "%,d";
|
String COMMAS = "%,d";
|
||||||
|
|
||||||
String DECIMAL1_COMMAS = "%,.1f";
|
String DECIMAL1_COMMAS = "%,.1f";
|
||||||
String DECIMAL2_COMMAS = "%,.2f";
|
String DECIMAL2_COMMAS = "%,.2f";
|
||||||
String DECIMAL3_COMMAS = "%,.3f";
|
String DECIMAL3_COMMAS = "%,.3f";
|
||||||
|
|
||||||
String DECIMAL1 = "%.1f";
|
String DECIMAL1 = "%.1f";
|
||||||
String DECIMAL2 = "%.2f";
|
String DECIMAL2 = "%.2f";
|
||||||
String DECIMAL3 = "%.3f";
|
String DECIMAL3 = "%.3f";
|
||||||
|
|
||||||
String CURRENCY = "$%,.2f";
|
String CURRENCY = "$%,.2f";
|
||||||
|
|
||||||
|
String PERCENT = "%.0f%%";
|
||||||
|
String PERCENT_POINT1 = "%.1f%%";
|
||||||
|
String PERCENT_POINT2 = "%.2f%%";
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@SuppressWarnings("checkstyle:Indentation")
|
||||||
|
static String getExcelFormat(String javaDisplayFormat)
|
||||||
|
{
|
||||||
|
if(javaDisplayFormat == null)
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch(javaDisplayFormat)
|
||||||
|
{
|
||||||
|
case DisplayFormat.DEFAULT -> null;
|
||||||
|
case DisplayFormat.COMMAS -> "#,##0";
|
||||||
|
case DisplayFormat.DECIMAL1 -> "0.0";
|
||||||
|
case DisplayFormat.DECIMAL2 -> "0.00";
|
||||||
|
case DisplayFormat.DECIMAL3 -> "0.000";
|
||||||
|
case DisplayFormat.DECIMAL1_COMMAS -> "#,##0.0";
|
||||||
|
case DisplayFormat.DECIMAL2_COMMAS -> "#,##0.00";
|
||||||
|
case DisplayFormat.DECIMAL3_COMMAS -> "#,##0.000";
|
||||||
|
case DisplayFormat.CURRENCY -> "$#,##0.00";
|
||||||
|
case DisplayFormat.PERCENT -> "0%";
|
||||||
|
case DisplayFormat.PERCENT_POINT1 -> "0.0%";
|
||||||
|
case DisplayFormat.PERCENT_POINT2 -> "0.00%";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,207 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.model.metadata.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Field within a report
|
||||||
|
*******************************************************************************/
|
||||||
|
public class QReportField
|
||||||
|
{
|
||||||
|
private String name;
|
||||||
|
private String label;
|
||||||
|
private String fieldName;
|
||||||
|
private String formula;
|
||||||
|
private String displayFormat;
|
||||||
|
// todo - type?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportField withName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for label
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getLabel()
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for label
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setLabel(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for label
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportField withLabel(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for fieldName
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getFieldName()
|
||||||
|
{
|
||||||
|
return fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for fieldName
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setFieldName(String fieldName)
|
||||||
|
{
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for fieldName
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportField withFieldName(String fieldName)
|
||||||
|
{
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for formula
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getFormula()
|
||||||
|
{
|
||||||
|
return formula;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for formula
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setFormula(String formula)
|
||||||
|
{
|
||||||
|
this.formula = formula;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for formula
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportField withFormula(String formula)
|
||||||
|
{
|
||||||
|
this.formula = formula;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for displayFormat
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getDisplayFormat()
|
||||||
|
{
|
||||||
|
return displayFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for displayFormat
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setDisplayFormat(String displayFormat)
|
||||||
|
{
|
||||||
|
this.displayFormat = displayFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for displayFormat
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportField withDisplayFormat(String displayFormat)
|
||||||
|
{
|
||||||
|
this.displayFormat = displayFormat;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.model.metadata.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Meta-data definition of a report generated by QQQ
|
||||||
|
*******************************************************************************/
|
||||||
|
public class QReportMetaData
|
||||||
|
{
|
||||||
|
private String name;
|
||||||
|
private String label;
|
||||||
|
private List<QFieldMetaData> inputFields;
|
||||||
|
private String sourceTable;
|
||||||
|
private QQueryFilter queryFilter;
|
||||||
|
private List<QReportView> views;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportMetaData withName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for label
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getLabel()
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for label
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setLabel(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for label
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportMetaData withLabel(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for inputFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<QFieldMetaData> getInputFields()
|
||||||
|
{
|
||||||
|
return inputFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for inputFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setInputFields(List<QFieldMetaData> inputFields)
|
||||||
|
{
|
||||||
|
this.inputFields = inputFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for inputFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportMetaData withInputFields(List<QFieldMetaData> inputFields)
|
||||||
|
{
|
||||||
|
this.inputFields = inputFields;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for sourceTable
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getSourceTable()
|
||||||
|
{
|
||||||
|
return sourceTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for sourceTable
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setSourceTable(String sourceTable)
|
||||||
|
{
|
||||||
|
this.sourceTable = sourceTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for sourceTable
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportMetaData withSourceTable(String sourceTable)
|
||||||
|
{
|
||||||
|
this.sourceTable = sourceTable;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for queryFilter
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QQueryFilter getQueryFilter()
|
||||||
|
{
|
||||||
|
return queryFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for queryFilter
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setQueryFilter(QQueryFilter queryFilter)
|
||||||
|
{
|
||||||
|
this.queryFilter = queryFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for queryFilter
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportMetaData withQueryFilter(QQueryFilter queryFilter)
|
||||||
|
{
|
||||||
|
this.queryFilter = queryFilter;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for views
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<QReportView> getViews()
|
||||||
|
{
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for views
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setViews(List<QReportView> views)
|
||||||
|
{
|
||||||
|
this.views = views;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for views
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportMetaData withViews(List<QReportView> views)
|
||||||
|
{
|
||||||
|
this.views = views;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,350 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.model.metadata.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class QReportView
|
||||||
|
{
|
||||||
|
private String name;
|
||||||
|
private String label;
|
||||||
|
private ReportType type;
|
||||||
|
private String titleFormat;
|
||||||
|
private List<String> titleFields;
|
||||||
|
private List<String> pivotFields;
|
||||||
|
private boolean totalRow = false;
|
||||||
|
private List<QReportField> columns;
|
||||||
|
private List<QFilterOrderBy> orderByFields;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportView withName(String name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for label
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getLabel()
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for label
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setLabel(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for label
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportView withLabel(String label)
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for type
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public ReportType getType()
|
||||||
|
{
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for type
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setType(ReportType type)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for type
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportView withType(ReportType type)
|
||||||
|
{
|
||||||
|
this.type = type;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for titleFormat
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getTitleFormat()
|
||||||
|
{
|
||||||
|
return titleFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for titleFormat
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setTitleFormat(String titleFormat)
|
||||||
|
{
|
||||||
|
this.titleFormat = titleFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for titleFormat
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportView withTitleFormat(String titleFormat)
|
||||||
|
{
|
||||||
|
this.titleFormat = titleFormat;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for titleFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<String> getTitleFields()
|
||||||
|
{
|
||||||
|
return titleFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for titleFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setTitleFields(List<String> titleFields)
|
||||||
|
{
|
||||||
|
this.titleFields = titleFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for titleFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportView withTitleFields(List<String> titleFields)
|
||||||
|
{
|
||||||
|
this.titleFields = titleFields;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for pivotFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<String> getPivotFields()
|
||||||
|
{
|
||||||
|
return pivotFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for pivotFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setPivotFields(List<String> pivotFields)
|
||||||
|
{
|
||||||
|
this.pivotFields = pivotFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for pivotFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportView withPivotFields(List<String> pivotFields)
|
||||||
|
{
|
||||||
|
this.pivotFields = pivotFields;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for totalRow
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean getTotalRow()
|
||||||
|
{
|
||||||
|
return totalRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for totalRow
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setTotalRow(boolean totalRow)
|
||||||
|
{
|
||||||
|
this.totalRow = totalRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for totalRow
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportView withTotalRow(boolean totalRow)
|
||||||
|
{
|
||||||
|
this.totalRow = totalRow;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for columns
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<QReportField> getColumns()
|
||||||
|
{
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for columns
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setColumns(List<QReportField> columns)
|
||||||
|
{
|
||||||
|
this.columns = columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for columns
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportView withColumns(List<QReportField> columns)
|
||||||
|
{
|
||||||
|
this.columns = columns;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for orderByFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public List<QFilterOrderBy> getOrderByFields()
|
||||||
|
{
|
||||||
|
return orderByFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for orderByFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setOrderByFields(List<QFilterOrderBy> orderByFields)
|
||||||
|
{
|
||||||
|
this.orderByFields = orderByFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for orderByFields
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public QReportView withOrderByFields(List<QFilterOrderBy> orderByFields)
|
||||||
|
{
|
||||||
|
this.orderByFields = orderByFields;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.model.metadata.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Types of reports that QQQ can generate
|
||||||
|
*******************************************************************************/
|
||||||
|
public enum ReportType
|
||||||
|
{
|
||||||
|
PIVOT,
|
||||||
|
SIMPLE
|
||||||
|
}
|
@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
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.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
import org.apache.commons.lang.NotImplementedException;
|
import org.apache.commons.lang.NotImplementedException;
|
||||||
|
|
||||||
|
|
||||||
@ -172,6 +173,16 @@ public class MemoryRecordStore
|
|||||||
recordMatches = !testIn(criterion, value);
|
recordMatches = !testIn(criterion, value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case IS_BLANK:
|
||||||
|
{
|
||||||
|
recordMatches = testBlank(criterion, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case IS_NOT_BLANK:
|
||||||
|
{
|
||||||
|
recordMatches = !testBlank(criterion, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case CONTAINS:
|
case CONTAINS:
|
||||||
{
|
{
|
||||||
recordMatches = testContains(criterion, fieldName, value);
|
recordMatches = testContains(criterion, fieldName, value);
|
||||||
@ -182,6 +193,26 @@ public class MemoryRecordStore
|
|||||||
recordMatches = !testContains(criterion, fieldName, value);
|
recordMatches = !testContains(criterion, fieldName, value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case STARTS_WITH:
|
||||||
|
{
|
||||||
|
recordMatches = testStartsWith(criterion, fieldName, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NOT_STARTS_WITH:
|
||||||
|
{
|
||||||
|
recordMatches = !testStartsWith(criterion, fieldName, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ENDS_WITH:
|
||||||
|
{
|
||||||
|
recordMatches = testEndsWith(criterion, fieldName, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NOT_ENDS_WITH:
|
||||||
|
{
|
||||||
|
recordMatches = !testEndsWith(criterion, fieldName, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case GREATER_THAN:
|
case GREATER_THAN:
|
||||||
{
|
{
|
||||||
recordMatches = testGreaterThan(criterion, value);
|
recordMatches = testGreaterThan(criterion, value);
|
||||||
@ -202,6 +233,22 @@ public class MemoryRecordStore
|
|||||||
recordMatches = !testGreaterThan(criterion, value);
|
recordMatches = !testGreaterThan(criterion, value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case BETWEEN:
|
||||||
|
{
|
||||||
|
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
|
||||||
|
QFilterCriteria criteria1 = new QFilterCriteria().withValues(new ArrayList<>(criterion.getValues()));
|
||||||
|
criteria1.getValues().remove(0);
|
||||||
|
recordMatches = (testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NOT_BETWEEN:
|
||||||
|
{
|
||||||
|
QFilterCriteria criteria0 = new QFilterCriteria().withValues(criterion.getValues());
|
||||||
|
QFilterCriteria criteria1 = new QFilterCriteria().withValues(criterion.getValues());
|
||||||
|
criteria1.getValues().remove(0);
|
||||||
|
recordMatches = !(testGreaterThan(criteria0, value) || testEquals(criteria0, value)) && (!testGreaterThan(criteria1, value) || testEquals(criteria1, value));
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
throw new NotImplementedException("Operator [" + criterion.getOperator() + "] is not yet implemented in the Memory backend.");
|
throw new NotImplementedException("Operator [" + criterion.getOperator() + "] is not yet implemented in the Memory backend.");
|
||||||
@ -218,6 +265,26 @@ public class MemoryRecordStore
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private boolean testBlank(QFilterCriteria criterion, Serializable value)
|
||||||
|
{
|
||||||
|
if(value == null)
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if("".equals(ValueUtils.getValueAsString(value)))
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -229,7 +296,7 @@ public class MemoryRecordStore
|
|||||||
throw (new IllegalArgumentException("Missing criterion value in query"));
|
throw (new IllegalArgumentException("Missing criterion value in query"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value == null)
|
if(value == null)
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
// a database would say 'false' for if a null column is > a value, so do the same. //
|
// a database would say 'false' for if a null column is > a value, so do the same. //
|
||||||
@ -247,6 +314,31 @@ public class MemoryRecordStore
|
|||||||
return (valueNumber.doubleValue() > criterionValueNumber.doubleValue());
|
return (valueNumber.doubleValue() > criterionValueNumber.doubleValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(value instanceof LocalDate || criterionValue instanceof LocalDate)
|
||||||
|
{
|
||||||
|
LocalDate valueDate;
|
||||||
|
if(value instanceof LocalDate ld)
|
||||||
|
{
|
||||||
|
valueDate = ld;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
valueDate = ValueUtils.getValueAsLocalDate(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate criterionDate;
|
||||||
|
if(criterionValue instanceof LocalDate ld)
|
||||||
|
{
|
||||||
|
criterionDate = ld;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
criterionDate = ValueUtils.getValueAsLocalDate(criterionValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (valueDate.isAfter(criterionDate));
|
||||||
|
}
|
||||||
|
|
||||||
throw (new NotImplementedException("Greater/Less Than comparisons are not (yet?) implemented for the supplied types [" + value.getClass().getSimpleName() + "][" + criterionValue.getClass().getSimpleName() + "]"));
|
throw (new NotImplementedException("Greater/Less Than comparisons are not (yet?) implemented for the supplied types [" + value.getClass().getSimpleName() + "][" + criterionValue.getClass().getSimpleName() + "]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,6 +395,42 @@ public class MemoryRecordStore
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private boolean testStartsWith(QFilterCriteria criterion, String fieldName, Serializable value)
|
||||||
|
{
|
||||||
|
String stringValue = getStringFieldValue(value, fieldName, criterion);
|
||||||
|
String criterionValue = getFirstStringCriterionValue(criterion);
|
||||||
|
|
||||||
|
if(!stringValue.startsWith(criterionValue))
|
||||||
|
{
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private boolean testEndsWith(QFilterCriteria criterion, String fieldName, Serializable value)
|
||||||
|
{
|
||||||
|
String stringValue = getStringFieldValue(value, fieldName, criterion);
|
||||||
|
String criterionValue = getFirstStringCriterionValue(criterion);
|
||||||
|
|
||||||
|
if(!stringValue.endsWith(criterionValue))
|
||||||
|
{
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.utils;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Simple container for two objects
|
||||||
|
*******************************************************************************/
|
||||||
|
public class Pair<A, B>
|
||||||
|
{
|
||||||
|
private A a;
|
||||||
|
private B b;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public Pair(A a, B b)
|
||||||
|
{
|
||||||
|
this.a = a;
|
||||||
|
this.b = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return (a + ":" + b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for a
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public A getA()
|
||||||
|
{
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for b
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public B getB()
|
||||||
|
{
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if(this == o)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(o == null || getClass() != o.getClass())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Pair<?, ?> pair = (Pair<?, ?>) o;
|
||||||
|
return Objects.equals(a, pair.a) && Objects.equals(b, pair.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return Objects.hash(a, b);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.utils.aggregates;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public interface AggregatesInterface<T extends Serializable>
|
||||||
|
{
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
void add(T t);
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
int getCount();
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
T getSum();
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
T getMin();
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
T getMax();
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
BigDecimal getAverage();
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.utils.aggregates;
|
||||||
|
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class BigDecimalAggregates implements AggregatesInterface<BigDecimal>
|
||||||
|
{
|
||||||
|
private int count = 0;
|
||||||
|
// private Integer countDistinct;
|
||||||
|
private BigDecimal sum;
|
||||||
|
private BigDecimal min;
|
||||||
|
private BigDecimal max;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Add a new value to this aggregate set
|
||||||
|
*******************************************************************************/
|
||||||
|
public void add(BigDecimal input)
|
||||||
|
{
|
||||||
|
if(input == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if(sum == null)
|
||||||
|
{
|
||||||
|
sum = input;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sum = sum.add(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(min == null || input.compareTo(min) < 0)
|
||||||
|
{
|
||||||
|
min = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(max == null || input.compareTo(max) > 0)
|
||||||
|
{
|
||||||
|
max = input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public int getCount()
|
||||||
|
{
|
||||||
|
return (count);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public BigDecimal getSum()
|
||||||
|
{
|
||||||
|
return (sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public BigDecimal getMin()
|
||||||
|
{
|
||||||
|
return (min);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public BigDecimal getMax()
|
||||||
|
{
|
||||||
|
return (max);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public BigDecimal getAverage()
|
||||||
|
{
|
||||||
|
if(this.count > 0)
|
||||||
|
{
|
||||||
|
return (BigDecimal.valueOf(this.sum.doubleValue() / (double) this.count));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.utils.aggregates;
|
||||||
|
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class IntegerAggregates implements AggregatesInterface<Integer>
|
||||||
|
{
|
||||||
|
private int count = 0;
|
||||||
|
// private Integer countDistinct;
|
||||||
|
private Integer sum;
|
||||||
|
private Integer min;
|
||||||
|
private Integer max;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Add a new value to this aggregate set
|
||||||
|
*******************************************************************************/
|
||||||
|
public void add(Integer input)
|
||||||
|
{
|
||||||
|
if(input == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if(sum == null)
|
||||||
|
{
|
||||||
|
sum = input;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sum = sum + input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(min == null || input < min)
|
||||||
|
{
|
||||||
|
min = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(max == null || input > max)
|
||||||
|
{
|
||||||
|
max = input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public int getCount()
|
||||||
|
{
|
||||||
|
return (count);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public Integer getSum()
|
||||||
|
{
|
||||||
|
return (sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public Integer getMin()
|
||||||
|
{
|
||||||
|
return (min);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public Integer getMax()
|
||||||
|
{
|
||||||
|
return (max);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public BigDecimal getAverage()
|
||||||
|
{
|
||||||
|
if(this.count > 0)
|
||||||
|
{
|
||||||
|
return (BigDecimal.valueOf(this.sum.doubleValue() / (double) this.count));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -30,9 +30,9 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
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.QFieldMetaData;
|
||||||
@ -50,7 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Unit test for the ReportAction
|
** Unit test for the ReportAction
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
class ReportActionTest
|
class ExportActionTest
|
||||||
{
|
{
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -120,22 +120,22 @@ class ReportActionTest
|
|||||||
{
|
{
|
||||||
try(FileOutputStream outputStream = new FileOutputStream(filename))
|
try(FileOutputStream outputStream = new FileOutputStream(filename))
|
||||||
{
|
{
|
||||||
ReportInput reportInput = new ReportInput(TestUtils.defineInstance(), TestUtils.getMockSession());
|
ExportInput exportInput = new ExportInput(TestUtils.defineInstance(), TestUtils.getMockSession());
|
||||||
reportInput.setTableName("person");
|
exportInput.setTableName("person");
|
||||||
QTableMetaData table = reportInput.getTable();
|
QTableMetaData table = exportInput.getTable();
|
||||||
|
|
||||||
reportInput.setReportFormat(reportFormat);
|
exportInput.setReportFormat(reportFormat);
|
||||||
reportInput.setReportOutputStream(outputStream);
|
exportInput.setReportOutputStream(outputStream);
|
||||||
reportInput.setQueryFilter(new QQueryFilter());
|
exportInput.setQueryFilter(new QQueryFilter());
|
||||||
reportInput.setLimit(recordCount);
|
exportInput.setLimit(recordCount);
|
||||||
|
|
||||||
if(specifyFields)
|
if(specifyFields)
|
||||||
{
|
{
|
||||||
reportInput.setFieldNames(table.getFields().values().stream().map(QFieldMetaData::getName).collect(Collectors.toList()));
|
exportInput.setFieldNames(table.getFields().values().stream().map(QFieldMetaData::getName).collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
ReportOutput reportOutput = new ReportAction().execute(reportInput);
|
ExportOutput exportOutput = new ExportAction().execute(exportInput);
|
||||||
assertNotNull(reportOutput);
|
assertNotNull(exportOutput);
|
||||||
assertEquals(recordCount, reportOutput.getRecordCount());
|
assertEquals(recordCount, exportOutput.getRecordCount());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,12 +147,12 @@ class ReportActionTest
|
|||||||
@Test
|
@Test
|
||||||
void testBadFieldNames()
|
void testBadFieldNames()
|
||||||
{
|
{
|
||||||
ReportInput reportInput = new ReportInput(TestUtils.defineInstance(), TestUtils.getMockSession());
|
ExportInput exportInput = new ExportInput(TestUtils.defineInstance(), TestUtils.getMockSession());
|
||||||
reportInput.setTableName("person");
|
exportInput.setTableName("person");
|
||||||
reportInput.setFieldNames(List.of("Foo", "Bar", "Baz"));
|
exportInput.setFieldNames(List.of("Foo", "Bar", "Baz"));
|
||||||
assertThrows(QUserFacingException.class, () ->
|
assertThrows(QUserFacingException.class, () ->
|
||||||
{
|
{
|
||||||
new ReportAction().execute(reportInput);
|
new ExportAction().execute(exportInput);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,15 +164,15 @@ class ReportActionTest
|
|||||||
@Test
|
@Test
|
||||||
void testPreExecuteCount() throws QException
|
void testPreExecuteCount() throws QException
|
||||||
{
|
{
|
||||||
ReportInput reportInput = new ReportInput(TestUtils.defineInstance(), TestUtils.getMockSession());
|
ExportInput exportInput = new ExportInput(TestUtils.defineInstance(), TestUtils.getMockSession());
|
||||||
reportInput.setTableName("person");
|
exportInput.setTableName("person");
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// use xlsx, which has a max-rows limit, to verify that code runs, but doesn't throw when there aren't too many rows //
|
// use xlsx, which has a max-rows limit, to verify that code runs, but doesn't throw when there aren't too many rows //
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
reportInput.setReportFormat(ReportFormat.XLSX);
|
exportInput.setReportFormat(ReportFormat.XLSX);
|
||||||
|
|
||||||
new ReportAction().preExecute(reportInput);
|
new ExportAction().preExecute(exportInput);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// nothing to assert - but if preExecute throws, then the test will fail. //
|
// nothing to assert - but if preExecute throws, then the test will fail. //
|
||||||
@ -198,17 +198,17 @@ class ReportActionTest
|
|||||||
QInstance qInstance = TestUtils.defineInstance();
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
qInstance.addTable(wideTable);
|
qInstance.addTable(wideTable);
|
||||||
|
|
||||||
ReportInput reportInput = new ReportInput(qInstance, TestUtils.getMockSession());
|
ExportInput exportInput = new ExportInput(qInstance, TestUtils.getMockSession());
|
||||||
reportInput.setTableName("wide");
|
exportInput.setTableName("wide");
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
// use xlsx, which has a max-cols limit, to verify that code. //
|
// use xlsx, which has a max-cols limit, to verify that code. //
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
reportInput.setReportFormat(ReportFormat.XLSX);
|
exportInput.setReportFormat(ReportFormat.XLSX);
|
||||||
|
|
||||||
assertThrows(QUserFacingException.class, () ->
|
assertThrows(QUserFacingException.class, () ->
|
||||||
{
|
{
|
||||||
new ReportAction().preExecute(reportInput);
|
new ExportAction().preExecute(exportInput);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
|
||||||
|
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||||
|
import org.assertj.core.data.Offset;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static com.kingsrook.qqq.backend.core.actions.reporting.FormulaInterpreter.interpretFormula;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for FormulaInterpreter
|
||||||
|
*******************************************************************************/
|
||||||
|
class FormulaInterpreterTest
|
||||||
|
{
|
||||||
|
public static final Offset<BigDecimal> ZERO_OFFSET = Offset.offset(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testInterpretFormulaSimpleSuccess() throws QFormulaException
|
||||||
|
{
|
||||||
|
QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter();
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("7"), interpretFormula(vi, "7"));
|
||||||
|
assertEquals(new BigDecimal("8"), interpretFormula(vi, "ADD(3,5)"));
|
||||||
|
assertEquals(new BigDecimal("9"), interpretFormula(vi, "ADD(2,ADD(3,4))"));
|
||||||
|
assertEquals(new BigDecimal("10"), interpretFormula(vi, "ADD(ADD(1,5),4)"));
|
||||||
|
assertEquals(new BigDecimal("11"), interpretFormula(vi, "ADD(ADD(1,5),ADD(2,3))"));
|
||||||
|
assertEquals(new BigDecimal("15"), interpretFormula(vi, "ADD(1,ADD(2,ADD(3,ADD(4,5))))"));
|
||||||
|
assertEquals(new BigDecimal("15"), interpretFormula(vi, "ADD(1,ADD(ADD(2,ADD(3,4)),5))"));
|
||||||
|
assertEquals(new BigDecimal("15"), interpretFormula(vi, "ADD(ADD(ADD(ADD(1,2),3),4),5)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testInterpretFormulaWithVariables() throws QFormulaException
|
||||||
|
{
|
||||||
|
QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter();
|
||||||
|
vi.addValueMap("input", Map.of("i", 5, "j", 6, "f", new BigDecimal("0.1")));
|
||||||
|
|
||||||
|
assertEquals("5", interpretFormula(vi, "${input.i}"));
|
||||||
|
assertEquals(new BigDecimal("8"), interpretFormula(vi, "ADD(3,${input.i})"));
|
||||||
|
assertEquals(new BigDecimal("11"), interpretFormula(vi, "ADD(${input.i},${input.j})"));
|
||||||
|
assertEquals(new BigDecimal("11.1"), interpretFormula(vi, "ADD(${input.f},ADD(${input.i},${input.j}))"));
|
||||||
|
assertEquals(new BigDecimal("11.2"), interpretFormula(vi, "ADD(ADD(${input.f},ADD(${input.i},${input.j})),${input.f})"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testInterpretFormulaRecursiveExceptions()
|
||||||
|
{
|
||||||
|
QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter();
|
||||||
|
vi.addValueMap("input", Map.of("i", 5, "c", 'c'));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> interpretFormula(vi, "")).hasMessageContaining("No results");
|
||||||
|
assertThatThrownBy(() -> interpretFormula(vi, "NOT-A-FUN(1,2)")).hasMessageContaining("unrecognized expression");
|
||||||
|
assertThatThrownBy(() -> interpretFormula(vi, "ADD(1)")).hasMessageContaining("Wrong number of arguments");
|
||||||
|
assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,2,3)")).hasMessageContaining("Wrong number of arguments");
|
||||||
|
assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,A)")).hasMessageContaining("[A] as a number");
|
||||||
|
assertThatThrownBy(() -> interpretFormula(vi, "ADD(1,${input.c})")).hasMessageContaining("[c] as a number");
|
||||||
|
// todo - bad syntax (e.g., missing ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testFunctions() throws QFormulaException
|
||||||
|
{
|
||||||
|
QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter();
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("3"), interpretFormula(vi, "ADD(1,2)"));
|
||||||
|
assertEquals(new BigDecimal("2"), interpretFormula(vi, "MINUS(4,2)"));
|
||||||
|
assertEquals(new BigDecimal("34.500"), interpretFormula(vi, "MULTIPLY(100,0.345)"));
|
||||||
|
|
||||||
|
assertThat((BigDecimal) interpretFormula(vi, "DIVIDE(1,2)")).isCloseTo(new BigDecimal("0.5"), ZERO_OFFSET);
|
||||||
|
assertNull(interpretFormula(vi, "DIVIDE(1,0)"));
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("0.5"), interpretFormula(vi, "ROUND(0.510,1)"));
|
||||||
|
assertEquals(new BigDecimal("5.0"), interpretFormula(vi, "ROUND(5.010,2)"));
|
||||||
|
assertEquals(new BigDecimal("5"), interpretFormula(vi, "ROUND(5.010,1)"));
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("0.5100"), interpretFormula(vi, "SCALE(0.510,4)"));
|
||||||
|
assertEquals(new BigDecimal("5.01"), interpretFormula(vi, "SCALE(5.010,2)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void test() throws QFormulaException
|
||||||
|
{
|
||||||
|
QMetaDataVariableInterpreter vi = new QMetaDataVariableInterpreter();
|
||||||
|
vi.addValueMap("pivot", Map.of("sum.noOfShoes", 5));
|
||||||
|
vi.addValueMap("total", Map.of("sum.noOfShoes", 18));
|
||||||
|
|
||||||
|
assertEquals(new BigDecimal("27.78"), interpretFormula(vi, "SCALE(MULTIPLY(100,DIVIDE_SCALE(${pivot.sum.noOfShoes},${total.sum.noOfShoes},6)),2)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,442 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.Month;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
|
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
|
||||||
|
import com.kingsrook.qqq.backend.core.testutils.PersonQRecord;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for GenerateReportAction
|
||||||
|
*******************************************************************************/
|
||||||
|
class GenerateReportActionTest
|
||||||
|
{
|
||||||
|
private static final String REPORT_NAME = "personReport1";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@BeforeEach
|
||||||
|
@AfterEach
|
||||||
|
void beforeAndAfterEach()
|
||||||
|
{
|
||||||
|
ListOfMapsExportStreamer.getList().clear();
|
||||||
|
MemoryRecordStore.getInstance().reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testPivot1() throws QException
|
||||||
|
{
|
||||||
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
|
qInstance.addReport(defineReport(true));
|
||||||
|
insertPersonRecords(qInstance);
|
||||||
|
runReport(qInstance, LocalDate.of(1980, Month.JANUARY, 1), LocalDate.of(1980, Month.DECEMBER, 31));
|
||||||
|
|
||||||
|
List<Map<String, String>> list = ListOfMapsExportStreamer.getList();
|
||||||
|
Iterator<Map<String, String>> iterator = list.iterator();
|
||||||
|
Map<String, String> row = iterator.next();
|
||||||
|
assertEquals(3, list.size());
|
||||||
|
assertThat(list.get(0)).containsOnlyKeys("Last Name", "Report Start Date", "Report End Date", "Person Count", "Quantity", "Revenue", "Cost", "Profit", "Cost Per", "% Total", "Margins", "Revenue Per", "Margin Per");
|
||||||
|
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Keller");
|
||||||
|
assertThat(row.get("Person Count")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("5");
|
||||||
|
assertThat(row.get("Report Start Date")).isEqualTo("1980-01-01");
|
||||||
|
assertThat(row.get("Report End Date")).isEqualTo("1980-12-31");
|
||||||
|
assertThat(row.get("Cost")).isEqualTo("3.50");
|
||||||
|
assertThat(row.get("Revenue")).isEqualTo("2.40");
|
||||||
|
assertThat(row.get("Cost Per")).isEqualTo("0.70");
|
||||||
|
assertThat(row.get("Revenue Per")).isEqualTo("0.48");
|
||||||
|
assertThat(row.get("Margin Per")).isEqualTo("-0.22");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Kelkhoff");
|
||||||
|
assertThat(row.get("Person Count")).isEqualTo("2");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("13");
|
||||||
|
assertThat(row.get("Cost")).isEqualTo("7.00"); // sum of the 2 Kelkhoff rows' costs
|
||||||
|
assertThat(row.get("Revenue")).isEqualTo("8.40"); // sum of the 2 Kelkhoff rows' price
|
||||||
|
assertThat(row.get("Cost Per")).isEqualTo("0.54"); // sum cost / quantity
|
||||||
|
assertThat(row.get("Revenue Per")).isEqualTo("0.65"); // sum price (Revenue) / quantity
|
||||||
|
assertThat(row.get("Margin Per")).isEqualTo("0.11"); // Revenue Per - Cost Per
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Totals");
|
||||||
|
assertThat(row.get("Person Count")).isEqualTo("3");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("18");
|
||||||
|
assertThat(row.get("Cost")).isEqualTo("10.50");
|
||||||
|
assertThat(row.get("Cost Per")).startsWith("0.58");
|
||||||
|
assertThat(row.get("Cost")).isEqualTo("10.50"); // sum of all 3 matching rows' costs
|
||||||
|
assertThat(row.get("Revenue")).isEqualTo("10.80"); // sum of all 3 matching rows' price
|
||||||
|
assertThat(row.get("Profit")).isEqualTo("0.30"); // Revenue - Cost
|
||||||
|
assertThat(row.get("Margins")).isEqualTo("0.03"); // 100*Profit / Revenue
|
||||||
|
assertThat(row.get("Cost Per")).isEqualTo("0.58"); // sum cost / quantity
|
||||||
|
assertThat(row.get("Revenue Per")).isEqualTo("0.60"); // sum price (Revenue) / quantity
|
||||||
|
assertThat(row.get("Margin Per")).isEqualTo("0.02"); // Revenue Per - Cost Per
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testPivot2() throws QException
|
||||||
|
{
|
||||||
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
|
QReportMetaData report = defineReport(false);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
// change from the default to sort reversed //
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
report.getViews().get(0).getOrderByFields().get(0).setIsAscending(false);
|
||||||
|
qInstance.addReport(report);
|
||||||
|
insertPersonRecords(qInstance);
|
||||||
|
runReport(qInstance, LocalDate.of(1980, Month.JANUARY, 1), LocalDate.of(1980, Month.DECEMBER, 31));
|
||||||
|
|
||||||
|
List<Map<String, String>> list = ListOfMapsExportStreamer.getList();
|
||||||
|
Iterator<Map<String, String>> iterator = list.iterator();
|
||||||
|
Map<String, String> row = iterator.next();
|
||||||
|
assertEquals(2, list.size());
|
||||||
|
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Kelkhoff");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("13");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Keller");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("5");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testPivot3() throws QException
|
||||||
|
{
|
||||||
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
|
QReportMetaData report = defineReport(false);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// remove the filters, change to sort by personCount (to get some ties), then sumPrice desc //
|
||||||
|
// this also shows the behavior of a null value in an order by //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
report.setQueryFilter(null);
|
||||||
|
report.getViews().get(0).setOrderByFields(List.of(new QFilterOrderBy("personCount"), new QFilterOrderBy("sumPrice", false)));
|
||||||
|
qInstance.addReport(report);
|
||||||
|
insertPersonRecords(qInstance);
|
||||||
|
runReport(qInstance, LocalDate.now(), LocalDate.now());
|
||||||
|
|
||||||
|
List<Map<String, String>> list = ListOfMapsExportStreamer.getList();
|
||||||
|
Iterator<Map<String, String>> iterator = list.iterator();
|
||||||
|
Map<String, String> row = iterator.next();
|
||||||
|
|
||||||
|
assertEquals(5, list.size());
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Keller");
|
||||||
|
assertThat(row.get("Person Count")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Revenue")).isEqualTo("2.40");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Kelly");
|
||||||
|
assertThat(row.get("Person Count")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Revenue")).isEqualTo("1.20");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Jones");
|
||||||
|
assertThat(row.get("Person Count")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Revenue")).isEqualTo("1.00");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Jonson");
|
||||||
|
assertThat(row.get("Person Count")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Revenue")).isNull();
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Kelkhoff");
|
||||||
|
assertThat(row.get("Person Count")).isEqualTo("2");
|
||||||
|
assertThat(row.get("Revenue")).isEqualTo("8.40");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testPivot4() throws QException
|
||||||
|
{
|
||||||
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
|
QReportMetaData report = defineReport(false);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// remove the filter, change to have 2 pivot columns - homeStateId and lastName - we should get no roll-up like this. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
report.setQueryFilter(null);
|
||||||
|
report.getViews().get(0).setPivotFields(List.of(
|
||||||
|
"homeStateId",
|
||||||
|
"lastName"
|
||||||
|
));
|
||||||
|
qInstance.addReport(report);
|
||||||
|
insertPersonRecords(qInstance);
|
||||||
|
runReport(qInstance, LocalDate.now(), LocalDate.now());
|
||||||
|
|
||||||
|
List<Map<String, String>> list = ListOfMapsExportStreamer.getList();
|
||||||
|
Iterator<Map<String, String>> iterator = list.iterator();
|
||||||
|
Map<String, String> row = iterator.next();
|
||||||
|
assertEquals(6, list.size());
|
||||||
|
|
||||||
|
assertThat(row.get("Home State Id")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Jonson");
|
||||||
|
assertThat(row.get("Quantity")).isNull();
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Home State Id")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Jones");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("3");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Home State Id")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Kelly");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("4");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Home State Id")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Keller");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("5");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Home State Id")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Kelkhoff");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("6");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Home State Id")).isEqualTo("2");
|
||||||
|
assertThat(row.get("Last Name")).isEqualTo("Kelkhoff");
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("7");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testPivot5() throws QException
|
||||||
|
{
|
||||||
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
|
QReportMetaData report = defineReport(false);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// remove the filter, and just pivot on homeStateId - should aggregate differently //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
report.setQueryFilter(null);
|
||||||
|
report.getViews().get(0).setPivotFields(List.of("homeStateId"));
|
||||||
|
qInstance.addReport(report);
|
||||||
|
insertPersonRecords(qInstance);
|
||||||
|
runReport(qInstance, LocalDate.now(), LocalDate.now());
|
||||||
|
|
||||||
|
List<Map<String, String>> list = ListOfMapsExportStreamer.getList();
|
||||||
|
Iterator<Map<String, String>> iterator = list.iterator();
|
||||||
|
Map<String, String> row = iterator.next();
|
||||||
|
assertEquals(2, list.size());
|
||||||
|
assertThat(row.get("Home State Id")).isEqualTo("2");
|
||||||
|
assertThat(row.get("Last Name")).isNull();
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("7");
|
||||||
|
|
||||||
|
row = iterator.next();
|
||||||
|
assertThat(row.get("Home State Id")).isEqualTo("1");
|
||||||
|
assertThat(row.get("Last Name")).isNull();
|
||||||
|
assertThat(row.get("Quantity")).isEqualTo("18");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void runToCsv() throws Exception
|
||||||
|
{
|
||||||
|
String name = "/tmp/report.csv";
|
||||||
|
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
|
||||||
|
{
|
||||||
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
|
qInstance.addReport(defineReport(true));
|
||||||
|
insertPersonRecords(qInstance);
|
||||||
|
|
||||||
|
ReportInput reportInput = new ReportInput(qInstance);
|
||||||
|
reportInput.setSession(new QSession());
|
||||||
|
reportInput.setReportName(REPORT_NAME);
|
||||||
|
reportInput.setReportFormat(ReportFormat.CSV);
|
||||||
|
reportInput.setReportOutputStream(fileOutputStream);
|
||||||
|
reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
|
||||||
|
new GenerateReportAction().execute(reportInput);
|
||||||
|
System.out.println("Wrote File: " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void runToXlsx() throws Exception
|
||||||
|
{
|
||||||
|
String name = "/tmp/report.xlsx";
|
||||||
|
try(FileOutputStream fileOutputStream = new FileOutputStream(name))
|
||||||
|
{
|
||||||
|
QInstance qInstance = TestUtils.defineInstance();
|
||||||
|
qInstance.addReport(defineReport(true));
|
||||||
|
insertPersonRecords(qInstance);
|
||||||
|
|
||||||
|
ReportInput reportInput = new ReportInput(qInstance);
|
||||||
|
reportInput.setSession(new QSession());
|
||||||
|
reportInput.setReportName(REPORT_NAME);
|
||||||
|
reportInput.setReportFormat(ReportFormat.XLSX);
|
||||||
|
reportInput.setReportOutputStream(fileOutputStream);
|
||||||
|
reportInput.setInputValues(Map.of("startDate", LocalDate.of(1970, Month.MAY, 15), "endDate", LocalDate.now()));
|
||||||
|
new GenerateReportAction().execute(reportInput);
|
||||||
|
System.out.println("Wrote File: " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void runReport(QInstance qInstance, LocalDate startDate, LocalDate endDate) throws QException
|
||||||
|
{
|
||||||
|
ReportInput reportInput = new ReportInput(qInstance);
|
||||||
|
reportInput.setSession(new QSession());
|
||||||
|
reportInput.setReportName(REPORT_NAME);
|
||||||
|
reportInput.setReportFormat(ReportFormat.LIST_OF_MAPS);
|
||||||
|
reportInput.setReportOutputStream(new ByteArrayOutputStream());
|
||||||
|
reportInput.setInputValues(Map.of("startDate", startDate, "endDate", endDate));
|
||||||
|
new GenerateReportAction().execute(reportInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private void insertPersonRecords(QInstance qInstance) throws QException
|
||||||
|
{
|
||||||
|
TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY), List.of(
|
||||||
|
new PersonQRecord().withLastName("Jonson").withBirthDate(LocalDate.of(1980, Month.JANUARY, 31)).withNoOfShoes(null).withHomeStateId(1).withPrice(null).withCost(new BigDecimal("0.50")), // wrong last initial
|
||||||
|
new PersonQRecord().withLastName("Jones").withBirthDate(LocalDate.of(1980, Month.JANUARY, 31)).withNoOfShoes(3).withHomeStateId(1).withPrice(new BigDecimal("1.00")).withCost(new BigDecimal("0.50")), // wrong last initial
|
||||||
|
new PersonQRecord().withLastName("Kelly").withBirthDate(LocalDate.of(1979, Month.DECEMBER, 30)).withNoOfShoes(4).withHomeStateId(1).withPrice(new BigDecimal("1.20")).withCost(new BigDecimal("0.50")), // bad birthdate
|
||||||
|
new PersonQRecord().withLastName("Keller").withBirthDate(LocalDate.of(1980, Month.JANUARY, 7)).withNoOfShoes(5).withHomeStateId(1).withPrice(new BigDecimal("2.40")).withCost(new BigDecimal("3.50")),
|
||||||
|
new PersonQRecord().withLastName("Kelkhoff").withBirthDate(LocalDate.of(1980, Month.FEBRUARY, 15)).withNoOfShoes(6).withHomeStateId(1).withPrice(new BigDecimal("3.60")).withCost(new BigDecimal("3.50")),
|
||||||
|
new PersonQRecord().withLastName("Kelkhoff").withBirthDate(LocalDate.of(1980, Month.MARCH, 20)).withNoOfShoes(7).withHomeStateId(2).withPrice(new BigDecimal("4.80")).withCost(new BigDecimal("3.50"))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private QReportMetaData defineReport(boolean includeTotalRow)
|
||||||
|
{
|
||||||
|
return new QReportMetaData()
|
||||||
|
.withName(REPORT_NAME)
|
||||||
|
.withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||||
|
.withInputFields(List.of(
|
||||||
|
new QFieldMetaData("startDate", QFieldType.DATE_TIME),
|
||||||
|
new QFieldMetaData("endDate", QFieldType.DATE_TIME)
|
||||||
|
))
|
||||||
|
.withQueryFilter(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.STARTS_WITH, List.of("K")))
|
||||||
|
.withCriteria(new QFilterCriteria("birthDate", QCriteriaOperator.BETWEEN, List.of("${input.startDate}", "${input.endDate}")))
|
||||||
|
)
|
||||||
|
.withViews(List.of(
|
||||||
|
new QReportView()
|
||||||
|
.withName("pivot")
|
||||||
|
.withType(ReportType.PIVOT)
|
||||||
|
.withPivotFields(List.of("lastName"))
|
||||||
|
.withTotalRow(includeTotalRow)
|
||||||
|
.withTitleFormat("Number of shoes - people born between %s and %s - pivot on LastName, sort by Quantity, Revenue DESC")
|
||||||
|
.withTitleFields(List.of("${input.startDate}", "${input.endDate}"))
|
||||||
|
.withOrderByFields(List.of(new QFilterOrderBy("shoeCount"), new QFilterOrderBy("sumPrice", false)))
|
||||||
|
.withColumns(List.of(
|
||||||
|
new QReportField().withName("reportStartDate").withLabel("Report Start Date").withFormula("${input.startDate}"),
|
||||||
|
new QReportField().withName("reportEndDate").withLabel("Report End Date").withFormula("${input.endDate}"),
|
||||||
|
new QReportField().withName("personCount").withLabel("Person Count").withFormula("${pivot.count.id}").withDisplayFormat(DisplayFormat.COMMAS),
|
||||||
|
new QReportField().withName("shoeCount").withLabel("Quantity").withFormula("${pivot.sum.noOfShoes}").withDisplayFormat(DisplayFormat.COMMAS),
|
||||||
|
// new QReportField().withName("percentOfTotal").withLabel("% Total").withFormula("=MULTIPLY(100,DIVIDE(${pivot.sum.noOfShoes},${total.sum.noOfShoes}))").withDisplayFormat(DisplayFormat.PERCENT_POINT2),
|
||||||
|
new QReportField().withName("percentOfTotal").withLabel("% Total").withFormula("=DIVIDE(${pivot.sum.noOfShoes},${total.sum.noOfShoes})").withDisplayFormat(DisplayFormat.PERCENT_POINT2),
|
||||||
|
new QReportField().withName("sumCost").withLabel("Cost").withFormula("${pivot.sum.cost}").withDisplayFormat(DisplayFormat.CURRENCY),
|
||||||
|
new QReportField().withName("sumPrice").withLabel("Revenue").withFormula("${pivot.sum.price}").withDisplayFormat(DisplayFormat.CURRENCY),
|
||||||
|
new QReportField().withName("profit").withLabel("Profit").withFormula("=MINUS(${pivot.sum.price},${pivot.sum.cost})").withDisplayFormat(DisplayFormat.CURRENCY),
|
||||||
|
// new QReportField().withName("margin").withLabel("Margins").withFormula("=SCALE(MULTIPLY(100,DIVIDE(MINUS(${pivot.sum.price},${pivot.sum.cost}),${pivot.sum.price})),0)").withDisplayFormat(DisplayFormat.PERCENT),
|
||||||
|
new QReportField().withName("margin").withLabel("Margins").withFormula("=SCALE(DIVIDE(MINUS(${pivot.sum.price},${pivot.sum.cost}),${pivot.sum.price}),2)").withDisplayFormat(DisplayFormat.PERCENT),
|
||||||
|
new QReportField().withName("costPerShoe").withLabel("Cost Per").withFormula("=DIVIDE_SCALE(${pivot.sum.cost},${pivot.sum.noOfShoes},2)").withDisplayFormat(DisplayFormat.CURRENCY),
|
||||||
|
new QReportField().withName("revenuePerShoe").withLabel("Revenue Per").withFormula("=DIVIDE_SCALE(${pivot.sum.price},${pivot.sum.noOfShoes},2)").withDisplayFormat(DisplayFormat.CURRENCY),
|
||||||
|
new QReportField().withName("marginPer").withLabel("Margin Per").withFormula("=MINUS(DIVIDE_SCALE(${pivot.sum.price},${pivot.sum.noOfShoes},2),DIVIDE_SCALE(${pivot.sum.cost},${pivot.sum.noOfShoes},2))").withDisplayFormat(DisplayFormat.CURRENCY)
|
||||||
|
))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -64,6 +64,15 @@ class QValueFormatterTest
|
|||||||
assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000")));
|
assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), new BigDecimal("1000")));
|
||||||
assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000));
|
assertEquals("1000", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.STRING), 1000));
|
||||||
|
|
||||||
|
assertEquals("1%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT), 1));
|
||||||
|
assertEquals("1%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT), new BigDecimal("1.0")));
|
||||||
|
assertEquals("1.0%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT1), 1));
|
||||||
|
assertEquals("1.1%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT1), new BigDecimal("1.1")));
|
||||||
|
assertEquals("1.1%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT1), new BigDecimal("1.12")));
|
||||||
|
assertEquals("1.00%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT2), 1));
|
||||||
|
assertEquals("1.10%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT2), new BigDecimal("1.1")));
|
||||||
|
assertEquals("1.12%", qValueFormatter.formatValue(new QFieldMetaData().withDisplayFormat(DisplayFormat.PERCENT_POINT2), new BigDecimal("1.12")));
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// this one flows through the exceptional cases //
|
// this one flows through the exceptional cases //
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
package com.kingsrook.qqq.backend.core.instances;
|
package com.kingsrook.qqq.backend.core.instances;
|
||||||
|
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
@ -162,6 +163,43 @@ class QMetaDataVariableInterpreterTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testValueMaps()
|
||||||
|
{
|
||||||
|
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||||
|
variableInterpreter.addValueMap("input", Map.of("foo", "bar", "amount", new BigDecimal("3.50")));
|
||||||
|
|
||||||
|
assertEquals("bar", variableInterpreter.interpretForObject("${input.foo}"));
|
||||||
|
assertEquals(new BigDecimal("3.50"), variableInterpreter.interpretForObject("${input.amount}"));
|
||||||
|
assertEquals("${input.x}", variableInterpreter.interpretForObject("${input.x}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testMultipleValueMaps()
|
||||||
|
{
|
||||||
|
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||||
|
variableInterpreter.addValueMap("input", Map.of("amount", new BigDecimal("3.50"), "x", "y"));
|
||||||
|
variableInterpreter.addValueMap("others", Map.of("foo", "fu", "amount", new BigDecimal("1.75")));
|
||||||
|
|
||||||
|
assertEquals("${input.foo}", variableInterpreter.interpretForObject("${input.foo}"));
|
||||||
|
assertEquals("fu", variableInterpreter.interpretForObject("${others.foo}"));
|
||||||
|
assertEquals(new BigDecimal("3.50"), variableInterpreter.interpretForObject("${input.amount}"));
|
||||||
|
assertEquals(new BigDecimal("1.75"), variableInterpreter.interpretForObject("${others.amount}"));
|
||||||
|
assertEquals("y", variableInterpreter.interpretForObject("${input.x}"));
|
||||||
|
assertEquals("${others.x}", variableInterpreter.interpretForObject("${others.x}"));
|
||||||
|
assertEquals("${input.nil}", variableInterpreter.interpretForObject("${input.nil}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.testutils;
|
||||||
|
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public class PersonQRecord extends QRecord
|
||||||
|
{
|
||||||
|
public PersonQRecord withLastName(String lastName)
|
||||||
|
{
|
||||||
|
setValue("lastName", lastName);
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public PersonQRecord withBirthDate(LocalDate birthDate)
|
||||||
|
{
|
||||||
|
setValue("birthDate", birthDate);
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public PersonQRecord withNoOfShoes(Integer noOfShoes)
|
||||||
|
{
|
||||||
|
setValue("noOfShoes", noOfShoes);
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public PersonQRecord withPrice(BigDecimal price)
|
||||||
|
{
|
||||||
|
setValue("price", price);
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public PersonQRecord withCost(BigDecimal cost)
|
||||||
|
{
|
||||||
|
setValue("cost", cost);
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public PersonQRecord withHomeStateId(int homeStateId)
|
||||||
|
{
|
||||||
|
setValue("homeStateId", homeStateId);
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -58,6 +58,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||||
@ -91,7 +92,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Utility class for backend-core test classes
|
** Utility class for backend-core test classes
|
||||||
**
|
** TODO - move to testutils package.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class TestUtils
|
public class TestUtils
|
||||||
{
|
{
|
||||||
@ -406,6 +407,9 @@ public class TestUtils
|
|||||||
.withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STATE))
|
.withField(new QFieldMetaData("homeStateId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STATE))
|
||||||
.withField(new QFieldMetaData("favoriteShapeId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_SHAPE))
|
.withField(new QFieldMetaData("favoriteShapeId", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_SHAPE))
|
||||||
.withField(new QFieldMetaData("customValue", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_CUSTOM))
|
.withField(new QFieldMetaData("customValue", QFieldType.INTEGER).withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_CUSTOM))
|
||||||
|
.withField(new QFieldMetaData("noOfShoes", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS))
|
||||||
|
.withField(new QFieldMetaData("cost", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY))
|
||||||
|
.withField(new QFieldMetaData("price", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.core.utils.aggregates;
|
||||||
|
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import org.assertj.core.data.Offset;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for Aggregates
|
||||||
|
*******************************************************************************/
|
||||||
|
class AggregatesTest
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testInteger()
|
||||||
|
{
|
||||||
|
IntegerAggregates aggregates = new IntegerAggregates();
|
||||||
|
|
||||||
|
assertEquals(0, aggregates.getCount());
|
||||||
|
assertNull(aggregates.getMin());
|
||||||
|
assertNull(aggregates.getMax());
|
||||||
|
assertNull(aggregates.getSum());
|
||||||
|
assertNull(aggregates.getAverage());
|
||||||
|
|
||||||
|
aggregates.add(5);
|
||||||
|
assertEquals(1, aggregates.getCount());
|
||||||
|
assertEquals(5, aggregates.getMin());
|
||||||
|
assertEquals(5, aggregates.getMax());
|
||||||
|
assertEquals(5, aggregates.getSum());
|
||||||
|
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("5"), Offset.offset(BigDecimal.ZERO));
|
||||||
|
|
||||||
|
aggregates.add(10);
|
||||||
|
assertEquals(2, aggregates.getCount());
|
||||||
|
assertEquals(5, aggregates.getMin());
|
||||||
|
assertEquals(10, aggregates.getMax());
|
||||||
|
assertEquals(15, aggregates.getSum());
|
||||||
|
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("7.5"), Offset.offset(BigDecimal.ZERO));
|
||||||
|
|
||||||
|
aggregates.add(15);
|
||||||
|
assertEquals(3, aggregates.getCount());
|
||||||
|
assertEquals(5, aggregates.getMin());
|
||||||
|
assertEquals(15, aggregates.getMax());
|
||||||
|
assertEquals(30, aggregates.getSum());
|
||||||
|
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10"), Offset.offset(BigDecimal.ZERO));
|
||||||
|
|
||||||
|
aggregates.add(null);
|
||||||
|
assertEquals(3, aggregates.getCount());
|
||||||
|
assertEquals(5, aggregates.getMin());
|
||||||
|
assertEquals(15, aggregates.getMax());
|
||||||
|
assertEquals(30, aggregates.getSum());
|
||||||
|
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10"), Offset.offset(BigDecimal.ZERO));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testBigDecimal()
|
||||||
|
{
|
||||||
|
BigDecimalAggregates aggregates = new BigDecimalAggregates();
|
||||||
|
|
||||||
|
assertEquals(0, aggregates.getCount());
|
||||||
|
assertNull(aggregates.getMin());
|
||||||
|
assertNull(aggregates.getMax());
|
||||||
|
assertNull(aggregates.getSum());
|
||||||
|
assertNull(aggregates.getAverage());
|
||||||
|
|
||||||
|
BigDecimal bd51 = new BigDecimal("5.1");
|
||||||
|
aggregates.add(bd51);
|
||||||
|
assertEquals(1, aggregates.getCount());
|
||||||
|
assertEquals(bd51, aggregates.getMin());
|
||||||
|
assertEquals(bd51, aggregates.getMax());
|
||||||
|
assertEquals(bd51, aggregates.getSum());
|
||||||
|
assertThat(aggregates.getAverage()).isCloseTo(bd51, Offset.offset(BigDecimal.ZERO));
|
||||||
|
|
||||||
|
BigDecimal bd101 = new BigDecimal("10.1");
|
||||||
|
aggregates.add(new BigDecimal("10.1"));
|
||||||
|
assertEquals(2, aggregates.getCount());
|
||||||
|
assertEquals(bd51, aggregates.getMin());
|
||||||
|
assertEquals(bd101, aggregates.getMax());
|
||||||
|
assertEquals(new BigDecimal("15.2"), aggregates.getSum());
|
||||||
|
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("7.6"), Offset.offset(BigDecimal.ZERO));
|
||||||
|
|
||||||
|
BigDecimal bd148 = new BigDecimal("14.8");
|
||||||
|
aggregates.add(bd148);
|
||||||
|
|
||||||
|
aggregates.add(null);
|
||||||
|
assertEquals(3, aggregates.getCount());
|
||||||
|
assertEquals(bd51, aggregates.getMin());
|
||||||
|
assertEquals(bd148, aggregates.getMax());
|
||||||
|
assertEquals(new BigDecimal("30.0"), aggregates.getSum());
|
||||||
|
assertThat(aggregates.getAverage()).isCloseTo(new BigDecimal("10.0"), Offset.offset(BigDecimal.ZERO));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -40,7 +40,7 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.WidgetDataLoader;
|
|||||||
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
|
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction;
|
import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
|
import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction;
|
import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||||
@ -61,8 +61,8 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataInpu
|
|||||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.ProcessMetaDataOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||||
@ -671,7 +671,6 @@ public class QJavalinImplementation
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Load the data for a widget of a given name.
|
** Load the data for a widget of a given name.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -692,6 +691,7 @@ public class QJavalinImplementation
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -756,22 +756,22 @@ public class QJavalinImplementation
|
|||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
// set up the report action's input object //
|
// set up the report action's input object //
|
||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
ReportInput reportInput = new ReportInput(qInstance);
|
ExportInput exportInput = new ExportInput(qInstance);
|
||||||
setupSession(context, reportInput);
|
setupSession(context, exportInput);
|
||||||
reportInput.setTableName(tableName);
|
exportInput.setTableName(tableName);
|
||||||
reportInput.setReportFormat(reportFormat);
|
exportInput.setReportFormat(reportFormat);
|
||||||
reportInput.setFilename(filename);
|
exportInput.setFilename(filename);
|
||||||
reportInput.setLimit(limit);
|
exportInput.setLimit(limit);
|
||||||
|
|
||||||
String fields = stringQueryParam(context, "fields");
|
String fields = stringQueryParam(context, "fields");
|
||||||
if(StringUtils.hasContent(fields))
|
if(StringUtils.hasContent(fields))
|
||||||
{
|
{
|
||||||
reportInput.setFieldNames(List.of(fields.split(",")));
|
exportInput.setFieldNames(List.of(fields.split(",")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(filter != null)
|
if(filter != null)
|
||||||
{
|
{
|
||||||
reportInput.setQueryFilter(JsonUtils.toObject(filter, QQueryFilter.class));
|
exportInput.setQueryFilter(JsonUtils.toObject(filter, QQueryFilter.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -782,10 +782,10 @@ public class QJavalinImplementation
|
|||||||
PipedOutputStream pipedOutputStream = new PipedOutputStream();
|
PipedOutputStream pipedOutputStream = new PipedOutputStream();
|
||||||
PipedInputStream pipedInputStream = new PipedInputStream();
|
PipedInputStream pipedInputStream = new PipedInputStream();
|
||||||
pipedOutputStream.connect(pipedInputStream);
|
pipedOutputStream.connect(pipedInputStream);
|
||||||
reportInput.setReportOutputStream(pipedOutputStream);
|
exportInput.setReportOutputStream(pipedOutputStream);
|
||||||
|
|
||||||
ReportAction reportAction = new ReportAction();
|
ExportAction exportAction = new ExportAction();
|
||||||
reportAction.preExecute(reportInput);
|
exportAction.preExecute(exportInput);
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// start the async job. //
|
// start the async job. //
|
||||||
@ -795,7 +795,7 @@ public class QJavalinImplementation
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
reportAction.execute(reportInput);
|
exportAction.execute(exportInput);
|
||||||
return (true);
|
return (true);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
|
@ -38,7 +38,7 @@ import java.util.Optional;
|
|||||||
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
|
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
|
import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.reporting.ReportAction;
|
import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||||
@ -58,9 +58,9 @@ import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
|
|||||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
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.processes.RunProcessOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping;
|
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QKeyBasedFieldMapping;
|
import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QKeyBasedFieldMapping;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||||
@ -633,25 +633,25 @@ public class QPicoCliImplementation
|
|||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
// set up the report action's input object //
|
// set up the report action's input object //
|
||||||
/////////////////////////////////////////////
|
/////////////////////////////////////////////
|
||||||
ReportInput reportInput = new ReportInput(qInstance);
|
ExportInput exportInput = new ExportInput(qInstance);
|
||||||
reportInput.setSession(session);
|
exportInput.setSession(session);
|
||||||
reportInput.setTableName(tableName);
|
exportInput.setTableName(tableName);
|
||||||
reportInput.setReportFormat(reportFormat);
|
exportInput.setReportFormat(reportFormat);
|
||||||
reportInput.setFilename(filename);
|
exportInput.setFilename(filename);
|
||||||
reportInput.setReportOutputStream(outputStream);
|
exportInput.setReportOutputStream(outputStream);
|
||||||
reportInput.setLimit(subParseResult.matchedOptionValue("limit", null));
|
exportInput.setLimit(subParseResult.matchedOptionValue("limit", null));
|
||||||
|
|
||||||
reportInput.setQueryFilter(generateQueryFilter(subParseResult));
|
exportInput.setQueryFilter(generateQueryFilter(subParseResult));
|
||||||
|
|
||||||
String fieldNames = subParseResult.matchedOptionValue("--fieldNames", "");
|
String fieldNames = subParseResult.matchedOptionValue("--fieldNames", "");
|
||||||
if(StringUtils.hasContent(fieldNames))
|
if(StringUtils.hasContent(fieldNames))
|
||||||
{
|
{
|
||||||
reportInput.setFieldNames(Arrays.asList(fieldNames.split(",")));
|
exportInput.setFieldNames(Arrays.asList(fieldNames.split(",")));
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportOutput reportOutput = new ReportAction().execute(reportInput);
|
ExportOutput exportOutput = new ExportAction().execute(exportInput);
|
||||||
|
|
||||||
commandLine.getOut().println("Wrote " + reportOutput.getRecordCount() + " records to file " + filename);
|
commandLine.getOut().println("Wrote " + exportOutput.getRecordCount() + " records to file " + filename);
|
||||||
return commandLine.getCommandSpec().exitCodeOnSuccess();
|
return commandLine.getCommandSpec().exitCodeOnSuccess();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
Reference in New Issue
Block a user