Feedback from code reviews

This commit is contained in:
2022-10-03 09:09:06 -05:00
parent 17cace070c
commit 3f84271a36
25 changed files with 303 additions and 284 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,7 @@ import java.math.BigDecimal;
/*******************************************************************************
**
** BigDecimal version of data aggregator
*******************************************************************************/
public class BigDecimalAggregates implements AggregatesInterface<BigDecimal>
{

View File

@ -26,7 +26,7 @@ import java.math.BigDecimal;
/*******************************************************************************
**
** Integer version of data aggregator
*******************************************************************************/
public class IntegerAggregates implements AggregatesInterface<Integer>
{

View File

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

View File

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

View File

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