mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
Feedback from code reviews
This commit is contained in:
@ -118,7 +118,7 @@ public class CsvExportStreamer implements ExportStreamerInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int addRecords(List<QRecord> qRecords) throws QReportingException
|
||||
public void addRecords(List<QRecord> qRecords) throws QReportingException
|
||||
{
|
||||
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||
|
||||
@ -126,7 +126,6 @@ public class CsvExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
writeRecord(qRecord);
|
||||
}
|
||||
return (qRecords.size());
|
||||
}
|
||||
|
||||
|
||||
|
@ -24,8 +24,10 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
@ -37,6 +39,7 @@ import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExc
|
||||
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.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.tables.QTableMetaData;
|
||||
@ -81,7 +84,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** display formats is a map of field name to Excel format strings (e.g., $#,##0.00)
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void setDisplayFormats(Map<String, String> displayFormats)
|
||||
@ -100,7 +103,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Starts a new worksheet in the current workbook. Can be called multiple times.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
|
||||
@ -114,9 +117,18 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
this.row = 0;
|
||||
this.sheetCount++;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is the first call in here (e.g., the workbook hasn't been opened yet), then open it now //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(workbook == null)
|
||||
{
|
||||
workbook = new Workbook(outputStream, "QQQ", null);
|
||||
String appName = "QQQ";
|
||||
QInstance instance = exportInput.getInstance();
|
||||
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
|
||||
{
|
||||
appName = instance.getBranding().getCompanyName();
|
||||
}
|
||||
workbook = new Workbook(outputStream, appName, null);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -128,7 +140,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
worksheet.finish();
|
||||
}
|
||||
|
||||
worksheet = workbook.newWorksheet(Objects.requireNonNullElse(label, "Sheet " + sheetCount));
|
||||
worksheet = workbook.newWorksheet(Objects.requireNonNullElse(label, "Sheet" + sheetCount));
|
||||
|
||||
writeTitleAndHeader();
|
||||
}
|
||||
@ -195,7 +207,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int addRecords(List<QRecord> qRecords) throws QReportingException
|
||||
public void addRecords(List<QRecord> qRecords) throws QReportingException
|
||||
{
|
||||
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||
|
||||
@ -221,8 +233,6 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
throw (new QReportingException("Error generating Excel report", e));
|
||||
}
|
||||
}
|
||||
|
||||
return (qRecords.size());
|
||||
}
|
||||
|
||||
|
||||
@ -284,6 +294,12 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
worksheet.value(row, col, d);
|
||||
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
|
||||
}
|
||||
else if(value instanceof Instant i)
|
||||
{
|
||||
// todo - what would be a better zone to use here?
|
||||
worksheet.value(row, col, i.atZone(ZoneId.systemDefault()));
|
||||
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
|
||||
}
|
||||
else
|
||||
{
|
||||
worksheet.value(row, col, ValueUtils.getValueAsString(value));
|
||||
|
@ -208,9 +208,9 @@ public class ExportAction
|
||||
lastReceivedRecordsAt = System.currentTimeMillis();
|
||||
nextSleepMillis = INIT_SLEEP_MS;
|
||||
|
||||
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||
int recordsConsumed = reportStreamer.addRecords(records);
|
||||
recordCount += recordsConsumed;
|
||||
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||
reportStreamer.addRecords(records);
|
||||
recordCount += records.size();
|
||||
|
||||
LOG.info(countFromPreExecute != null
|
||||
? String.format("Processed %,d of %,d records so far", recordCount, countFromPreExecute)
|
||||
@ -237,9 +237,9 @@ public class ExportAction
|
||||
///////////////////////////////////////////////////
|
||||
// send the final records to the report streamer //
|
||||
///////////////////////////////////////////////////
|
||||
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||
int recordsConsumed = reportStreamer.addRecords(records);
|
||||
recordCount += recordsConsumed;
|
||||
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||
reportStreamer.addRecords(records);
|
||||
recordCount += records.size();
|
||||
|
||||
long reportEndTime = System.currentTimeMillis();
|
||||
LOG.info((countFromPreExecute != null
|
||||
|
@ -43,7 +43,7 @@ public interface ExportStreamerInterface
|
||||
/*******************************************************************************
|
||||
** Called as records flow into the pipe.
|
||||
******************************************************************************/
|
||||
int addRecords(List<QRecord> recordList) throws QReportingException;
|
||||
void addRecords(List<QRecord> recordList) throws QReportingException;
|
||||
|
||||
/*******************************************************************************
|
||||
** Called once, after all rows are available. Meant to write a footer, or close resources, for example.
|
||||
|
@ -36,17 +36,21 @@ 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.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Helper for Generating reports - to interpret formulas in report columns,
|
||||
** that are in "excel-style", ala: =MINUS(47,42) or
|
||||
** =IF(LT(ADD(${input.x},${input.y}),10,Yes,No)
|
||||
*******************************************************************************/
|
||||
public class FormulaInterpreter
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** public method to interpret a formula. Takes a variableInterpreter, optionally
|
||||
** full of maps of variables, and the formula string, assumed to have its leading
|
||||
** '=' char already trimmed away.
|
||||
*******************************************************************************/
|
||||
public static Serializable interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula) throws QFormulaException
|
||||
{
|
||||
@ -75,12 +79,14 @@ public class FormulaInterpreter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Recursive method that does the work of interpreting a formula.
|
||||
** Uses AtomicInteger `i` to track index through the string into and out of
|
||||
** recursive calls.
|
||||
*******************************************************************************/
|
||||
public static List<Serializable> interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula, AtomicInteger i) throws QFormulaException
|
||||
static List<Serializable> interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula, AtomicInteger i) throws QFormulaException
|
||||
{
|
||||
StringBuilder functionName = new StringBuilder();
|
||||
List<Serializable> result = new ArrayList<>();
|
||||
StringBuilder token = new StringBuilder();
|
||||
List<Serializable> result = new ArrayList<>();
|
||||
|
||||
char previousChar = 0;
|
||||
while(i.get() < formula.length())
|
||||
@ -92,22 +98,24 @@ public class FormulaInterpreter
|
||||
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 //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// open paren means: go into a sub-parse. Get back a list of arguments, and use those //
|
||||
// as arguments for the current token, which must be a function name then. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<Serializable> args = interpretFormula(variableInterpreter, formula, i);
|
||||
Serializable evaluate = evaluate(functionName.toString(), args, variableInterpreter);
|
||||
Serializable evaluate = evaluate(token.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. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// close paren means: end this sub-parse. evaluate the current token, //
|
||||
// add it to the result list, and return the result list. //
|
||||
// unless we just closed a paren - then we can just return. //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
if(previousChar != ')')
|
||||
{
|
||||
Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter);
|
||||
Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
|
||||
result.add(evaluate);
|
||||
}
|
||||
return (result);
|
||||
@ -115,22 +123,22 @@ public class FormulaInterpreter
|
||||
else if(c == ',')
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// comma means: evaluate the current thing; add it to the result list //
|
||||
// unless we just closed a paren. //
|
||||
// comma means: evaluate the current token; add it to the result list //
|
||||
// unless we just closed a paren - then we can just return. //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
if(previousChar != ')')
|
||||
{
|
||||
Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter);
|
||||
Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
|
||||
result.add(evaluate);
|
||||
}
|
||||
functionName = new StringBuilder();
|
||||
token = new StringBuilder();
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////
|
||||
// else, we add this char to the current name //
|
||||
////////////////////////////////////////////////
|
||||
functionName.append(c);
|
||||
/////////////////////////////////////////////////
|
||||
// else, we add this char to the current token //
|
||||
/////////////////////////////////////////////////
|
||||
token.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,9 +147,9 @@ public class FormulaInterpreter
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(result.isEmpty())
|
||||
{
|
||||
if(!functionName.isEmpty())
|
||||
if(!token.isEmpty())
|
||||
{
|
||||
Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter);
|
||||
Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
|
||||
result.add(evaluate);
|
||||
}
|
||||
}
|
||||
@ -152,12 +160,12 @@ public class FormulaInterpreter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Evaluate a token - maybe a literal, or variable, or function name -
|
||||
** with arguments if it's a function, and in the context of the variableInterpreter.
|
||||
*******************************************************************************/
|
||||
private static Serializable evaluate(String functionName, List<Serializable> args, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
|
||||
private static Serializable evaluate(String token, List<Serializable> args, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
|
||||
{
|
||||
// System.out.format("== Evaluating [%s](%s) ==\n", functionName, args);
|
||||
switch(functionName)
|
||||
switch(token)
|
||||
{
|
||||
case "ADD":
|
||||
{
|
||||
@ -209,7 +217,13 @@ public class FormulaInterpreter
|
||||
}
|
||||
case "IF":
|
||||
{
|
||||
// IF(CONDITION,TRUE,ELSE)
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// IF(CONDITION,TRUE,ELSE) //
|
||||
// behavior in a spreadsheet appears to be: //
|
||||
// booleans are evaluated naturally. //
|
||||
// strings - if they look like 'true' or 'false, they are evaluated, else they error //
|
||||
// numbers - 0 is false, all else are true. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
List<Serializable> actualArgs = getArgumentList(args, 3, variableInterpreter);
|
||||
Serializable condition = actualArgs.get(0);
|
||||
boolean conditionBoolean;
|
||||
@ -237,7 +251,7 @@ public class FormulaInterpreter
|
||||
}
|
||||
else
|
||||
{
|
||||
conditionBoolean = StringUtils.hasContent(s);
|
||||
throw (new QFormulaException("Could not evaluate string '" + s + "' as a boolean."));
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -276,7 +290,7 @@ public class FormulaInterpreter
|
||||
{
|
||||
try
|
||||
{
|
||||
return (ValueUtils.getValueAsBigDecimal(functionName));
|
||||
return (ValueUtils.getValueAsBigDecimal(token));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -285,7 +299,7 @@ public class FormulaInterpreter
|
||||
|
||||
try
|
||||
{
|
||||
return (variableInterpreter.interpret(functionName));
|
||||
return (variableInterpreter.interpret(token));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -295,13 +309,13 @@ public class FormulaInterpreter
|
||||
}
|
||||
}
|
||||
|
||||
throw (new QFormulaException("Unable to evaluate unrecognized expression: " + functionName + ""));
|
||||
throw (new QFormulaException("Unable to evaluate unrecognized expression: " + token + ""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** if any number in the list is null, get back null - else, return the result of the supplier.
|
||||
*******************************************************************************/
|
||||
private static Serializable nullIfAnyNullArgsElseBigDecimal(List<BigDecimal> numbers, Supplier<BigDecimal> supplier)
|
||||
{
|
||||
@ -315,7 +329,7 @@ public class FormulaInterpreter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** if any number in the list is null, get back null - else, return the result of the supplier.
|
||||
*******************************************************************************/
|
||||
private static Serializable nullIfAnyNullArgsElseBoolean(List<BigDecimal> numbers, Supplier<Boolean> supplier)
|
||||
{
|
||||
@ -329,7 +343,9 @@ public class FormulaInterpreter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** given a list of arguments, get back a specific number of arguments, all of which we
|
||||
** validate to be numbers (e.g., possibly interpreted variables) - else we throw.
|
||||
** also throw if not the right number is present.
|
||||
*******************************************************************************/
|
||||
private static List<BigDecimal> getNumberArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
|
||||
{
|
||||
@ -360,7 +376,8 @@ public class FormulaInterpreter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** given a list of arguments, get back a specific number of arguments, all of which we
|
||||
** get interpreted. throw if not the right number of args is present.
|
||||
*******************************************************************************/
|
||||
private static List<Serializable> getArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
|
||||
{
|
||||
@ -375,15 +392,8 @@ public class FormulaInterpreter
|
||||
List<Serializable> rs = new ArrayList<>();
|
||||
for(Serializable originalArg : originalArgs)
|
||||
{
|
||||
try
|
||||
{
|
||||
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
|
||||
rs.add(interpretedArg);
|
||||
}
|
||||
catch(QValueException e)
|
||||
{
|
||||
throw (new QFormulaException("Could not process [" + originalArg + "] as a number"));
|
||||
}
|
||||
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
|
||||
rs.add(interpretedArg);
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
@ -73,21 +73,28 @@ import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
|
||||
** Action to generate a report.
|
||||
**
|
||||
** A report can contain 1 or more Data Sources - e.g., tables + filters that define
|
||||
** data that goes into the report.
|
||||
** data that goes into the report, or simple data-supplier lambdas.
|
||||
**
|
||||
** A report can also contain 1 or more Views - e.g., sheets in a spreadsheet workbook.
|
||||
** (how do those work in non-XLSX formats??). Views can either be plain tables,
|
||||
** summaries (like pivot tables, but called summary to avoid confusion with "native"
|
||||
** pivot tables), or native pivot tables (not initially supported, due to lack of
|
||||
** support in fastexcel...).
|
||||
** (how do those work in non-XLSX formats??). Views can either be:
|
||||
** - plain tables,
|
||||
** - summaries (like pivot tables, but called summary to avoid confusion with "native" pivot tables),
|
||||
** - native pivot tables (not initially supported, due to lack of support in fastexcel...).
|
||||
*******************************************************************************/
|
||||
public class GenerateReportAction
|
||||
{
|
||||
//////////////////////////////////////////////////
|
||||
// viewName > PivotKey > fieldName > Aggregates //
|
||||
//////////////////////////////////////////////////
|
||||
Map<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> pivotAggregates = new HashMap<>();
|
||||
Map<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> variancePivotAggregates = new HashMap<>();
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// summaryAggregates and varianceAggregates are multi-level maps, ala: //
|
||||
// viewName > SummaryKey > fieldName > Aggregates //
|
||||
// e.g.: //
|
||||
// viewName: salesSummaryReport //
|
||||
// SummaryKey: [(state:MO),(city:St.Louis)] //
|
||||
// fieldName: salePrice //
|
||||
// Aggregates: (count:47;sum:10,000;max:2,000;min:15) //
|
||||
// salesSummaryReport > [(state:MO),(city:St.Louis)] > salePrice > (count:47;sum:10,000;max:2,000;min:15) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> summaryAggregates = new HashMap<>();
|
||||
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> varianceAggregates = new HashMap<>();
|
||||
|
||||
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>();
|
||||
Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>();
|
||||
@ -120,7 +127,7 @@ public class GenerateReportAction
|
||||
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
|
||||
.toList();
|
||||
|
||||
List<QReportView> dataSourcePivotViews = report.getViews().stream()
|
||||
List<QReportView> dataSourceSummaryViews = report.getViews().stream()
|
||||
.filter(v -> v.getType().equals(ReportType.SUMMARY))
|
||||
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
|
||||
.toList();
|
||||
@ -130,14 +137,15 @@ public class GenerateReportAction
|
||||
.filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName()))
|
||||
.toList();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this data source isn't used for any table views, but it is used for one or more pivot views (possibly as a variant), then run the query, gathering pivot data. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this data source isn't used for any table views, but it is used for one or //
|
||||
// more summary views (possibly as a variant), then run the query, gathering summary data. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(dataSourceTableViews.isEmpty())
|
||||
{
|
||||
if(!dataSourcePivotViews.isEmpty() || !dataSourceVariantViews.isEmpty())
|
||||
if(!dataSourceSummaryViews.isEmpty() || !dataSourceVariantViews.isEmpty())
|
||||
{
|
||||
gatherData(reportInput, dataSource, null, dataSourcePivotViews, dataSourceVariantViews);
|
||||
gatherData(reportInput, dataSource, null, dataSourceSummaryViews, dataSourceVariantViews);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -164,12 +172,12 @@ public class GenerateReportAction
|
||||
// start the table-view (e.g., open this tab in xlsx) and then run the query-loop //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
startTableView(reportInput, dataSource, dataSourceTableView);
|
||||
gatherData(reportInput, dataSource, dataSourceTableView, dataSourcePivotViews, dataSourceVariantViews);
|
||||
gatherData(reportInput, dataSource, dataSourceTableView, dataSourceSummaryViews, dataSourceVariantViews);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputPivots(reportInput);
|
||||
outputSummaries(reportInput);
|
||||
}
|
||||
|
||||
|
||||
@ -189,7 +197,7 @@ public class GenerateReportAction
|
||||
exportInput.setReportFormat(reportFormat);
|
||||
exportInput.setFilename(reportInput.getFilename());
|
||||
exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
|
||||
exportInput.setIncludeHeaderRow(reportView.getHeaderRow());
|
||||
exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow());
|
||||
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
||||
|
||||
List<QFieldMetaData> fields;
|
||||
@ -226,7 +234,7 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List<QReportView> pivotViews, List<QReportView> variantViews) throws QException
|
||||
private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check if this view has a transform step - if so, set it up now and run its pre-run //
|
||||
@ -304,7 +312,7 @@ public class GenerateReportAction
|
||||
records = finalTransformStepOutput.getRecords();
|
||||
}
|
||||
|
||||
return (consumeRecords(reportInput, dataSource, records, tableView, pivotViews, variantViews));
|
||||
return (consumeRecords(reportInput, dataSource, records, tableView, summaryViews, variantViews));
|
||||
});
|
||||
|
||||
////////////////////////////////////////////////
|
||||
@ -352,7 +360,7 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Integer consumeRecords(ReportInput reportInput, QReportDataSource dataSource, List<QRecord> records, QReportView tableView, List<QReportView> pivotViews, List<QReportView> variantViews) throws QException
|
||||
private Integer consumeRecords(ReportInput reportInput, QReportDataSource dataSource, List<QRecord> records, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
|
||||
{
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
|
||||
@ -364,14 +372,14 @@ public class GenerateReportAction
|
||||
reportStreamer.addRecords(records);
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// do aggregates for pivots //
|
||||
//////////////////////////////
|
||||
if(pivotViews != null)
|
||||
/////////////////////////////////
|
||||
// do aggregates for summaries //
|
||||
/////////////////////////////////
|
||||
if(summaryViews != null)
|
||||
{
|
||||
for(QReportView pivotView : pivotViews)
|
||||
for(QReportView summaryView : summaryViews)
|
||||
{
|
||||
addRecordsToPivotAggregates(pivotView, table, records, pivotAggregates);
|
||||
addRecordsToSummaryAggregates(summaryView, table, records, summaryAggregates);
|
||||
}
|
||||
}
|
||||
|
||||
@ -379,14 +387,14 @@ public class GenerateReportAction
|
||||
{
|
||||
for(QReportView variantView : variantViews)
|
||||
{
|
||||
addRecordsToPivotAggregates(variantView, table, records, variancePivotAggregates);
|
||||
addRecordsToSummaryAggregates(variantView, table, records, varianceAggregates);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////
|
||||
// do totals too, if any views want them //
|
||||
///////////////////////////////////////////
|
||||
if(pivotViews != null && pivotViews.stream().anyMatch(QReportView::getTotalRow))
|
||||
if(summaryViews != null && summaryViews.stream().anyMatch(QReportView::getIncludeTotalRow))
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
@ -394,7 +402,7 @@ public class GenerateReportAction
|
||||
}
|
||||
}
|
||||
|
||||
if(variantViews != null && variantViews.stream().anyMatch(QReportView::getTotalRow))
|
||||
if(variantViews != null && variantViews.stream().anyMatch(QReportView::getIncludeTotalRow))
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
@ -410,33 +418,33 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordsToPivotAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> aggregatesMap)
|
||||
private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> aggregatesMap)
|
||||
{
|
||||
Map<PivotKey, Map<String, AggregatesInterface<?>>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
|
||||
Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
PivotKey key = new PivotKey();
|
||||
for(String pivotField : view.getPivotFields())
|
||||
SummaryKey key = new SummaryKey();
|
||||
for(String summaryField : view.getPivotFields())
|
||||
{
|
||||
Serializable pivotValue = record.getValue(pivotField);
|
||||
if(table.getField(pivotField).getPossibleValueSourceName() != null)
|
||||
Serializable summaryValue = record.getValue(summaryField);
|
||||
if(table.getField(summaryField).getPossibleValueSourceName() != null)
|
||||
{
|
||||
pivotValue = record.getDisplayValue(pivotField);
|
||||
summaryValue = record.getDisplayValue(summaryField);
|
||||
}
|
||||
key.add(pivotField, pivotValue);
|
||||
key.add(summaryField, summaryValue);
|
||||
|
||||
if(view.getPivotSubTotals() && key.getKeys().size() < view.getPivotFields().size())
|
||||
if(view.getIncludePivotSubTotals() && key.getKeys().size() < view.getPivotFields().size())
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// be careful here, with these key objects, and their identity, being used as map keys //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
PivotKey subKey = key.clone();
|
||||
addRecordToPivotKeyAggregates(table, record, viewAggregates, subKey);
|
||||
SummaryKey subKey = key.clone();
|
||||
addRecordToSummaryKeyAggregates(table, record, viewAggregates, subKey);
|
||||
}
|
||||
}
|
||||
|
||||
addRecordToPivotKeyAggregates(table, record, viewAggregates, key);
|
||||
addRecordToSummaryKeyAggregates(table, record, viewAggregates, key);
|
||||
}
|
||||
}
|
||||
|
||||
@ -445,7 +453,7 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordToPivotKeyAggregates(QTableMetaData table, QRecord record, Map<PivotKey, Map<String, AggregatesInterface<?>>> viewAggregates, PivotKey key)
|
||||
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates, SummaryKey key)
|
||||
{
|
||||
Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
|
||||
addRecordToAggregatesMap(table, record, keyAggregates);
|
||||
@ -481,31 +489,31 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void outputPivots(ReportInput reportInput) throws QReportingException, QFormulaException
|
||||
private void outputSummaries(ReportInput reportInput) throws QReportingException, QFormulaException
|
||||
{
|
||||
List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
|
||||
for(QReportView view : reportViews)
|
||||
{
|
||||
QReportDataSource dataSource = report.getDataSource(view.getDataSourceName());
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
PivotOutput pivotOutput = computePivotRowsForView(reportInput, view, table);
|
||||
QReportDataSource dataSource = report.getDataSource(view.getDataSourceName());
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
|
||||
|
||||
ExportInput exportInput = new ExportInput(reportInput.getInstance());
|
||||
exportInput.setSession(reportInput.getSession());
|
||||
exportInput.setReportFormat(reportFormat);
|
||||
exportInput.setFilename(reportInput.getFilename());
|
||||
exportInput.setTitleRow(pivotOutput.titleRow);
|
||||
exportInput.setIncludeHeaderRow(view.getHeaderRow());
|
||||
exportInput.setTitleRow(summaryOutput.titleRow);
|
||||
exportInput.setIncludeHeaderRow(view.getIncludeHeaderRow());
|
||||
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
||||
|
||||
reportStreamer.setDisplayFormats(getDisplayFormatMap(view));
|
||||
reportStreamer.start(exportInput, getFields(table, view), view.getLabel());
|
||||
|
||||
reportStreamer.addRecords(pivotOutput.pivotRows); // todo - what if this set is huge?
|
||||
reportStreamer.addRecords(summaryOutput.summaryRows); // todo - what if this set is huge?
|
||||
|
||||
if(pivotOutput.totalRow != null)
|
||||
if(summaryOutput.totalRow != null)
|
||||
{
|
||||
reportStreamer.addTotalsRow(pivotOutput.totalRow);
|
||||
reportStreamer.addTotalsRow(summaryOutput.totalRow);
|
||||
}
|
||||
}
|
||||
|
||||
@ -561,70 +569,60 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private PivotOutput computePivotRowsForView(ReportInput reportInput, QReportView view, QTableMetaData table) throws QReportingException, QFormulaException
|
||||
private SummaryOutput computeSummaryRowsForView(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));
|
||||
variableInterpreter.addValueMap("total", getSummaryValuesForInterpreter(totalAggregates));
|
||||
|
||||
///////////
|
||||
// title //
|
||||
///////////
|
||||
String title = getTitle(view, variableInterpreter);
|
||||
|
||||
/////////////
|
||||
// headers //
|
||||
/////////////
|
||||
for(String field : view.getPivotFields())
|
||||
/////////////////////////
|
||||
// create summary rows //
|
||||
/////////////////////////
|
||||
List<QRecord> summaryRows = new ArrayList<>();
|
||||
for(Map.Entry<SummaryKey, Map<String, AggregatesInterface<?>>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
|
||||
{
|
||||
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();
|
||||
SummaryKey summaryKey = entry.getKey();
|
||||
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
|
||||
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates));
|
||||
Map<String, Serializable> summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
|
||||
variableInterpreter.addValueMap("pivot", summaryValues);
|
||||
variableInterpreter.addValueMap("summary", summaryValues);
|
||||
|
||||
HashMap<String, Serializable> thisRowValues = new HashMap<>();
|
||||
variableInterpreter.addValueMap("thisRow", thisRowValues);
|
||||
|
||||
if(!variancePivotAggregates.isEmpty())
|
||||
if(!varianceAggregates.isEmpty())
|
||||
{
|
||||
Map<PivotKey, Map<String, AggregatesInterface<?>>> varianceMap = variancePivotAggregates.getOrDefault(view.getName(), Collections.emptyMap());
|
||||
Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(pivotKey, Collections.emptyMap());
|
||||
variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceSubMap));
|
||||
Map<SummaryKey, Map<String, AggregatesInterface<?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
|
||||
Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
|
||||
Map<String, Serializable> varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
|
||||
variableInterpreter.addValueMap("variancePivot", varianceValues);
|
||||
variableInterpreter.addValueMap("variance", varianceValues);
|
||||
}
|
||||
|
||||
QRecord pivotRow = new QRecord();
|
||||
pivotRows.add(pivotRow);
|
||||
QRecord summaryRow = new QRecord();
|
||||
summaryRows.add(summaryRow);
|
||||
|
||||
//////////////////////////
|
||||
// add the pivot values //
|
||||
//////////////////////////
|
||||
for(Pair<String, Serializable> key : pivotKey.getKeys())
|
||||
////////////////////////////
|
||||
// add the summary values //
|
||||
////////////////////////////
|
||||
for(Pair<String, Serializable> key : summaryKey.getKeys())
|
||||
{
|
||||
pivotRow.setValue(key.getA(), key.getB());
|
||||
summaryRow.setValue(key.getA(), key.getB());
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// for pivot subtotals, add the text "Total" to the last field in this key //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
if(pivotKey.getKeys().size() < view.getPivotFields().size())
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// for summary subtotals, add the text "Total" to the last field in this key //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
if(summaryKey.getKeys().size() < view.getPivotFields().size())
|
||||
{
|
||||
String fieldName = pivotKey.getKeys().get(pivotKey.getKeys().size() - 1).getA();
|
||||
pivotRow.setValue(fieldName, pivotRow.getValueString(fieldName) + " Total");
|
||||
String fieldName = summaryKey.getKeys().get(summaryKey.getKeys().size() - 1).getA();
|
||||
summaryRow.setValue(fieldName, summaryRow.getValueString(fieldName) + " Total");
|
||||
}
|
||||
|
||||
///////////////////////////
|
||||
@ -633,47 +631,29 @@ public class GenerateReportAction
|
||||
for(QReportField column : view.getColumns())
|
||||
{
|
||||
Serializable serializable = getValueForColumn(variableInterpreter, column);
|
||||
pivotRow.setValue(column.getName(), serializable);
|
||||
summaryRow.setValue(column.getName(), serializable);
|
||||
thisRowValues.put(column.getName(), serializable);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// sort the pivot rows //
|
||||
/////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// sort the summary rows //
|
||||
// Note - this will NOT work correctly if there's more than 1 pivot field, as we're //
|
||||
// not doing anything to keep related rows them together (e.g., all MO state rows) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(view.getOrderByFields()))
|
||||
{
|
||||
pivotRows.sort((o1, o2) ->
|
||||
summaryRows.sort((o1, o2) ->
|
||||
{
|
||||
return pivotRowComparator(view, o1, o2);
|
||||
return summaryRowComparator(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())
|
||||
if(view.getIncludeTotalRow())
|
||||
{
|
||||
totalRow = new QRecord();
|
||||
|
||||
@ -682,16 +662,17 @@ public class GenerateReportAction
|
||||
if(totalRow.getValues().isEmpty())
|
||||
{
|
||||
totalRow.setValue(pivotField, "Totals");
|
||||
System.out.printf("%-15s", "Totals");
|
||||
}
|
||||
else
|
||||
{
|
||||
System.out.printf("%-15s", "");
|
||||
}
|
||||
}
|
||||
|
||||
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates));
|
||||
variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceTotalAggregates));
|
||||
Map<String, Serializable> totalValues = getSummaryValuesForInterpreter(totalAggregates);
|
||||
variableInterpreter.addValueMap("pivot", totalValues);
|
||||
variableInterpreter.addValueMap("summary", totalValues);
|
||||
|
||||
Map<String, Serializable> varianceTotalValues = getSummaryValuesForInterpreter(varianceTotalAggregates);
|
||||
variableInterpreter.addValueMap("variancePivot", varianceTotalValues);
|
||||
variableInterpreter.addValueMap("variance", varianceTotalValues);
|
||||
|
||||
HashMap<String, Serializable> thisRowValues = new HashMap<>();
|
||||
variableInterpreter.addValueMap("thisRow", thisRowValues);
|
||||
|
||||
@ -702,13 +683,10 @@ public class GenerateReportAction
|
||||
thisRowValues.put(column.getName(), serializable);
|
||||
|
||||
String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable);
|
||||
System.out.printf("%25s", formatted);
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
return (new PivotOutput(pivotRows, title, totalRow));
|
||||
return (new SummaryOutput(summaryRows, title, totalRow));
|
||||
}
|
||||
|
||||
|
||||
@ -734,10 +712,6 @@ public class GenerateReportAction
|
||||
title = view.getTitleFormat();
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(title))
|
||||
{
|
||||
System.out.println(title);
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
@ -767,7 +741,7 @@ public class GenerateReportAction
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private int pivotRowComparator(QReportView view, QRecord o1, QRecord o2)
|
||||
private int summaryRowComparator(QReportView view, QRecord o1, QRecord o2)
|
||||
{
|
||||
if(o1 == o2)
|
||||
{
|
||||
@ -807,26 +781,28 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Map<String, Serializable> getPivotValuesForInterpreter(Map<String, AggregatesInterface<?>> fieldAggregates)
|
||||
private Map<String, Serializable> getSummaryValuesForInterpreter(Map<String, AggregatesInterface<?>> fieldAggregates)
|
||||
{
|
||||
Map<String, Serializable> pivotValuesForInterpreter = new HashMap<>();
|
||||
Map<String, Serializable> summaryValuesForInterpreter = 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
|
||||
summaryValuesForInterpreter.put("sum." + fieldName, aggregates.getSum());
|
||||
summaryValuesForInterpreter.put("count." + fieldName, aggregates.getCount());
|
||||
summaryValuesForInterpreter.put("min." + fieldName, aggregates.getMin());
|
||||
summaryValuesForInterpreter.put("max." + fieldName, aggregates.getMax());
|
||||
summaryValuesForInterpreter.put("average." + fieldName, aggregates.getAverage());
|
||||
}
|
||||
return pivotValuesForInterpreter;
|
||||
return summaryValuesForInterpreter;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** record to serve as tuple/multi-value output of outputPivot method.
|
||||
** record to serve as tuple/multi-value output of computeSummaryRowsForView method.
|
||||
*******************************************************************************/
|
||||
private record PivotOutput(List<QRecord> pivotRows, String titleRow, QRecord totalRow)
|
||||
private record SummaryOutput(List<QRecord> summaryRows, String titleRow, QRecord totalRow)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -113,15 +113,13 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int addRecords(List<QRecord> qRecords) throws QReportingException
|
||||
public void addRecords(List<QRecord> qRecords) throws QReportingException
|
||||
{
|
||||
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||
|
||||
for(QRecord qRecord : qRecords)
|
||||
{
|
||||
addRecord(qRecord);
|
||||
}
|
||||
return (qRecords.size());
|
||||
}
|
||||
|
||||
|
||||
|
@ -32,7 +32,7 @@ import com.kingsrook.qqq.backend.core.utils.Pair;
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class PivotKey implements Cloneable
|
||||
public class SummaryKey implements Cloneable
|
||||
{
|
||||
private List<Pair<String, Serializable>> keys = new ArrayList<>();
|
||||
|
||||
@ -41,7 +41,7 @@ public class PivotKey implements Cloneable
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public PivotKey()
|
||||
public SummaryKey()
|
||||
{
|
||||
}
|
||||
|
||||
@ -93,8 +93,8 @@ public class PivotKey implements Cloneable
|
||||
{
|
||||
return false;
|
||||
}
|
||||
PivotKey pivotKey = (PivotKey) o;
|
||||
return Objects.equals(keys, pivotKey.keys);
|
||||
SummaryKey summaryKey = (SummaryKey) o;
|
||||
return Objects.equals(keys, summaryKey.keys);
|
||||
}
|
||||
|
||||
|
||||
@ -114,9 +114,9 @@ public class PivotKey implements Cloneable
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public PivotKey clone()
|
||||
public SummaryKey clone()
|
||||
{
|
||||
PivotKey clone = new PivotKey();
|
||||
SummaryKey clone = new SummaryKey();
|
||||
|
||||
for(Pair<String, Serializable> key : keys)
|
||||
{
|
@ -28,7 +28,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Interface for customizer on a QReportView. Extends Function by adding setter
|
||||
** method for reportInput.
|
||||
*******************************************************************************/
|
||||
public interface ReportViewCustomizer extends Function<QReportView, QReportView>
|
||||
{
|
||||
|
@ -28,7 +28,7 @@ import org.dhatim.fastexcel.StyleSetter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Version of excel styler that does bold headers and footers, with basic borders.
|
||||
*******************************************************************************/
|
||||
public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
|
||||
{
|
||||
|
@ -26,7 +26,8 @@ import org.dhatim.fastexcel.StyleSetter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Interface for classes that know how to apply styles to an Excel stream being
|
||||
** built by fastexcel.
|
||||
*******************************************************************************/
|
||||
public interface ExcelStylerInterface
|
||||
{
|
||||
|
@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
|
||||
*******************************************************************************/
|
||||
public class PlainExcelStyler implements ExcelStylerInterface
|
||||
{
|
||||
|
@ -51,7 +51,7 @@ public class QValueFormatter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** For a field, and its value, apply the field's displayFormat.
|
||||
*******************************************************************************/
|
||||
public String formatValue(QFieldMetaData field, Serializable value)
|
||||
{
|
||||
@ -61,7 +61,7 @@ public class QValueFormatter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** For a display format string (e.g., %d), and a value, apply the displayFormat.
|
||||
*******************************************************************************/
|
||||
public String formatValue(String displayFormat, Serializable value)
|
||||
{
|
||||
@ -71,7 +71,8 @@ public class QValueFormatter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** For a display format string, an optional fieldName (only used for logging),
|
||||
** and a value, apply the format.
|
||||
*******************************************************************************/
|
||||
private String formatValue(String displayFormat, String fieldName, Serializable value)
|
||||
{
|
||||
@ -159,9 +160,6 @@ public class QValueFormatter
|
||||
return (formatRecordLabelExceptionalCases(table, record));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// get list of values, then pass them to the string formatter method //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
return formatStringWithFields(table.getRecordLabelFormat(), table.getRecordLabelFields(), record.getDisplayValues(), record.getValues());
|
||||
@ -175,9 +173,10 @@ public class QValueFormatter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** For a given format string, and a list of fields, look in displayValueMap and
|
||||
** rawValueMap to get the values to apply to the format.
|
||||
*******************************************************************************/
|
||||
public String formatStringWithFields(String formatString, List<String> formatFields, Map<String, String> displayValueMap, Map<String, Serializable> rawValueMap)
|
||||
private String formatStringWithFields(String formatString, List<String> formatFields, Map<String, String> displayValueMap, Map<String, Serializable> rawValueMap)
|
||||
{
|
||||
List<Serializable> values = formatFields.stream()
|
||||
.map(fieldName ->
|
||||
@ -200,7 +199,8 @@ public class QValueFormatter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** For a given format string, and a list of values, apply the format. Note, null
|
||||
** values in the list become "".
|
||||
*******************************************************************************/
|
||||
public String formatStringWithValues(String formatString, List<String> formatValues)
|
||||
{
|
||||
|
@ -227,14 +227,14 @@ public class QInstanceEnricher
|
||||
field.setLabel(nameToLabel(field.getName()));
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// if this field has a possibleValueSource //
|
||||
// and that PVS exists in the instance //
|
||||
// and it's a table-type PVS and the table name is set //
|
||||
// and it's a valid table in the instant, and the table is in some app //
|
||||
// and the field doesn't have a LINK adornment //
|
||||
// then add a link-to-record-from-table adornment to the field. //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// if this field has a possibleValueSource //
|
||||
// and that PVS exists in the instance //
|
||||
// and it's a table-type PVS and the table name is set //
|
||||
// and it's a valid table in the instance, and the table is in some app //
|
||||
// and the field doesn't have a LINK adornment //
|
||||
// then add a link-to-record-from-table adornment to the field. //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
|
||||
{
|
||||
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
|
||||
|
@ -149,13 +149,17 @@ public class QMetaDataVariableInterpreter
|
||||
*******************************************************************************/
|
||||
public Serializable interpretForObject(String value)
|
||||
{
|
||||
return (interpretForObject(value, value));
|
||||
return (interpretForObject(value, null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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,
|
||||
** getting back the specified default if the string looks like a variable, but can't
|
||||
** be found. Where "looks like" means, for example, started with "${env." and ended
|
||||
** with "}", but wasn't set in the environment, or, more interestingly, based on the
|
||||
** valueMaps - only if the name to the left of the dot is an actual valueMap name.
|
||||
**
|
||||
** If input is null, output is null.
|
||||
** If input looks like ${env.X}, then the return value is the value of the env variable 'X'
|
||||
@ -175,14 +179,16 @@ public class QMetaDataVariableInterpreter
|
||||
if(value.startsWith(envPrefix) && value.endsWith("}"))
|
||||
{
|
||||
String envVarName = value.substring(envPrefix.length()).replaceFirst("}$", "");
|
||||
return (getEnvironmentVariable(envVarName));
|
||||
String result = getEnvironmentVariable(envVarName);
|
||||
return (result == null ? defaultIfLooksLikeVariableButNotFound : result);
|
||||
}
|
||||
|
||||
String propPrefix = "${prop.";
|
||||
if(value.startsWith(propPrefix) && value.endsWith("}"))
|
||||
{
|
||||
String propertyName = value.substring(propPrefix.length()).replaceFirst("}$", "");
|
||||
return (System.getProperty(propertyName));
|
||||
String result = System.getProperty(propertyName);
|
||||
return (result == null ? defaultIfLooksLikeVariableButNotFound : result);
|
||||
}
|
||||
|
||||
String literalPrefix = "${literal.";
|
||||
|
@ -26,7 +26,7 @@ import java.io.Serializable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Interface for objects that can be output from a process to summarize its results.
|
||||
*******************************************************************************/
|
||||
public interface ProcessSummaryLineInterface extends Serializable
|
||||
{
|
||||
@ -39,7 +39,8 @@ public interface ProcessSummaryLineInterface extends Serializable
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** meant to be called by framework, after process is complete, give the
|
||||
** summary object a chance to finalize itself before it's sent to a frontend.
|
||||
*******************************************************************************/
|
||||
default void prepareForFrontend(boolean isForResultScreen)
|
||||
{
|
||||
|
@ -26,7 +26,8 @@ import java.io.Serializable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Simple process summary result object, that lets you give a link to a record
|
||||
** in a table. e.g., if your process built such a record, give a link to it.
|
||||
*******************************************************************************/
|
||||
public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
|
||||
{
|
||||
@ -105,7 +106,7 @@ public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
|
||||
this.status = status;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
@ -27,7 +27,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Meta-data definition of a source of data for a report (e.g., a table and query
|
||||
** filter or custom-code reference).
|
||||
*******************************************************************************/
|
||||
public class QReportDataSource
|
||||
{
|
||||
|
@ -33,17 +33,19 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
*******************************************************************************/
|
||||
public class QReportView implements Cloneable
|
||||
{
|
||||
private String name;
|
||||
private String label;
|
||||
private String dataSourceName;
|
||||
private String varianceDataSourceName;
|
||||
private ReportType type;
|
||||
private String titleFormat;
|
||||
private List<String> titleFields;
|
||||
private List<String> pivotFields;
|
||||
private boolean headerRow = true;
|
||||
private boolean totalRow = false;
|
||||
private boolean pivotSubTotals = false;
|
||||
private String name;
|
||||
private String label;
|
||||
private String dataSourceName;
|
||||
private String varianceDataSourceName;
|
||||
private ReportType type;
|
||||
private String titleFormat;
|
||||
private List<String> titleFields;
|
||||
private List<String> pivotFields;
|
||||
|
||||
private boolean includeHeaderRow = true;
|
||||
private boolean includeTotalRow = false;
|
||||
private boolean includePivotSubTotals = false;
|
||||
|
||||
private List<QReportField> columns;
|
||||
private List<QFilterOrderBy> orderByFields;
|
||||
|
||||
@ -332,9 +334,9 @@ public class QReportView implements Cloneable
|
||||
** Getter for headerRow
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean getHeaderRow()
|
||||
public boolean getIncludeHeaderRow()
|
||||
{
|
||||
return headerRow;
|
||||
return includeHeaderRow;
|
||||
}
|
||||
|
||||
|
||||
@ -343,9 +345,9 @@ public class QReportView implements Cloneable
|
||||
** Setter for headerRow
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setHeaderRow(boolean headerRow)
|
||||
public void setIncludeHeaderRow(boolean includeHeaderRow)
|
||||
{
|
||||
this.headerRow = headerRow;
|
||||
this.includeHeaderRow = includeHeaderRow;
|
||||
}
|
||||
|
||||
|
||||
@ -354,9 +356,9 @@ public class QReportView implements Cloneable
|
||||
** Fluent setter for headerRow
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QReportView withHeaderRow(boolean headerRow)
|
||||
public QReportView withIncludeHeaderRow(boolean headerRow)
|
||||
{
|
||||
this.headerRow = headerRow;
|
||||
this.includeHeaderRow = headerRow;
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -366,9 +368,9 @@ public class QReportView implements Cloneable
|
||||
** Getter for totalRow
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean getTotalRow()
|
||||
public boolean getIncludeTotalRow()
|
||||
{
|
||||
return totalRow;
|
||||
return includeTotalRow;
|
||||
}
|
||||
|
||||
|
||||
@ -377,9 +379,9 @@ public class QReportView implements Cloneable
|
||||
** Setter for totalRow
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setTotalRow(boolean totalRow)
|
||||
public void setIncludeTotalRow(boolean includeTotalRow)
|
||||
{
|
||||
this.totalRow = totalRow;
|
||||
this.includeTotalRow = includeTotalRow;
|
||||
}
|
||||
|
||||
|
||||
@ -388,9 +390,9 @@ public class QReportView implements Cloneable
|
||||
** Fluent setter for totalRow
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QReportView withTotalRow(boolean totalRow)
|
||||
public QReportView withIncludeTotalRow(boolean totalRow)
|
||||
{
|
||||
this.totalRow = totalRow;
|
||||
this.includeTotalRow = totalRow;
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -400,9 +402,9 @@ public class QReportView implements Cloneable
|
||||
** Getter for pivotSubTotals
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean getPivotSubTotals()
|
||||
public boolean getIncludePivotSubTotals()
|
||||
{
|
||||
return pivotSubTotals;
|
||||
return includePivotSubTotals;
|
||||
}
|
||||
|
||||
|
||||
@ -411,9 +413,9 @@ public class QReportView implements Cloneable
|
||||
** Setter for pivotSubTotals
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setPivotSubTotals(boolean pivotSubTotals)
|
||||
public void setIncludePivotSubTotals(boolean includePivotSubTotals)
|
||||
{
|
||||
this.pivotSubTotals = pivotSubTotals;
|
||||
this.includePivotSubTotals = includePivotSubTotals;
|
||||
}
|
||||
|
||||
|
||||
@ -422,9 +424,9 @@ public class QReportView implements Cloneable
|
||||
** Fluent setter for pivotSubTotals
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QReportView withPivotSubTotals(boolean pivotSubTotals)
|
||||
public QReportView withIncludePivotSubTotals(boolean pivotSubTotals)
|
||||
{
|
||||
this.pivotSubTotals = pivotSubTotals;
|
||||
this.includePivotSubTotals = pivotSubTotals;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,8 @@ import java.math.BigDecimal;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Classes that support doing data aggregations (e.g., count, sum, min, max, average).
|
||||
** Sub-classes should supply the type parameter.
|
||||
*******************************************************************************/
|
||||
public interface AggregatesInterface<T extends Serializable>
|
||||
{
|
||||
|
@ -26,7 +26,7 @@ import java.math.BigDecimal;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** BigDecimal version of data aggregator
|
||||
*******************************************************************************/
|
||||
public class BigDecimalAggregates implements AggregatesInterface<BigDecimal>
|
||||
{
|
||||
|
@ -26,7 +26,7 @@ import java.math.BigDecimal;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Integer version of data aggregator
|
||||
*******************************************************************************/
|
||||
public class IntegerAggregates implements AggregatesInterface<Integer>
|
||||
{
|
||||
|
@ -191,6 +191,12 @@ class FormulaInterpreterTest
|
||||
|
||||
assertEquals("Yes", interpretFormula(vi, "IF(GT(${input.one},0),Yes,No)"));
|
||||
assertEquals("No", interpretFormula(vi, "IF(LT(${input.one},0),Yes,No)"));
|
||||
assertEquals("Yes", interpretFormula(vi, "IF(true,Yes,No)"));
|
||||
assertEquals("Yes", interpretFormula(vi, "IF(True,Yes,No)"));
|
||||
assertEquals("No", interpretFormula(vi, "IF(false,Yes,No)"));
|
||||
assertEquals("No", interpretFormula(vi, "IF(False,Yes,No)"));
|
||||
|
||||
assertThatThrownBy(() -> interpretFormula(vi, "IF(foo,Yes,No)")).hasRootCauseMessage("Could not evaluate string 'foo' as a boolean.");
|
||||
}
|
||||
|
||||
}
|
@ -423,7 +423,7 @@ public class GenerateReportActionTest
|
||||
.withDataSourceName("persons")
|
||||
.withType(ReportType.SUMMARY)
|
||||
.withPivotFields(List.of("lastName"))
|
||||
.withTotalRow(includeTotalRow)
|
||||
.withIncludeTotalRow(includeTotalRow)
|
||||
.withTitleFormat("Number of shoes - people born between %s and %s - pivot on LastName, sort by Quantity, Revenue DESC")
|
||||
.withTitleFields(List.of("${input.startDate}", "${input.endDate}"))
|
||||
.withOrderByFields(List.of(new QFilterOrderBy("shoeCount"), new QFilterOrderBy("sumPrice", false)))
|
||||
|
@ -174,7 +174,7 @@ class QMetaDataVariableInterpreterTest
|
||||
|
||||
assertEquals("bar", variableInterpreter.interpretForObject("${input.foo}"));
|
||||
assertEquals(new BigDecimal("3.50"), variableInterpreter.interpretForObject("${input.amount}"));
|
||||
assertEquals("${input.x}", variableInterpreter.interpretForObject("${input.x}"));
|
||||
assertNull(variableInterpreter.interpretForObject("${input.x}"));
|
||||
}
|
||||
|
||||
|
||||
@ -189,13 +189,13 @@ class QMetaDataVariableInterpreterTest
|
||||
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}"));
|
||||
assertNull(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}"));
|
||||
assertNull(variableInterpreter.interpretForObject("${others.x}"));
|
||||
assertNull(variableInterpreter.interpretForObject("${input.nil}"));
|
||||
}
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user