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 @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"); LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
@ -126,7 +126,6 @@ public class CsvExportStreamer implements ExportStreamerInterface
{ {
writeRecord(qRecord); 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.OutputStream;
import java.io.Serializable; import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Date; import java.util.Date;
import java.util.HashMap; 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.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -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 @Override
public void setDisplayFormats(Map<String, String> displayFormats) 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 @Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException 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.row = 0;
this.sheetCount++; 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) 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);
} }
///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
@ -195,7 +207,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
** **
*******************************************************************************/ *******************************************************************************/
@Override @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"); 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)); 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.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set(); 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 else
{ {
worksheet.value(row, col, ValueUtils.getValueAsString(value)); worksheet.value(row, col, ValueUtils.getValueAsString(value));

View File

@ -209,8 +209,8 @@ public class ExportAction
nextSleepMillis = INIT_SLEEP_MS; nextSleepMillis = INIT_SLEEP_MS;
List<QRecord> records = recordPipe.consumeAvailableRecords(); List<QRecord> records = recordPipe.consumeAvailableRecords();
int recordsConsumed = reportStreamer.addRecords(records); reportStreamer.addRecords(records);
recordCount += recordsConsumed; recordCount += records.size();
LOG.info(countFromPreExecute != null LOG.info(countFromPreExecute != null
? String.format("Processed %,d of %,d records so far", recordCount, countFromPreExecute) ? String.format("Processed %,d of %,d records so far", recordCount, countFromPreExecute)
@ -238,8 +238,8 @@ public class ExportAction
// send the final records to the report streamer // // send the final records to the report streamer //
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
List<QRecord> records = recordPipe.consumeAvailableRecords(); List<QRecord> records = recordPipe.consumeAvailableRecords();
int recordsConsumed = reportStreamer.addRecords(records); reportStreamer.addRecords(records);
recordCount += recordsConsumed; recordCount += records.size();
long reportEndTime = System.currentTimeMillis(); long reportEndTime = System.currentTimeMillis();
LOG.info((countFromPreExecute != null LOG.info((countFromPreExecute != null

View File

@ -43,7 +43,7 @@ public interface ExportStreamerInterface
/******************************************************************************* /*******************************************************************************
** Called as records flow into the pipe. ** 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. ** 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.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import 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 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 public static Serializable interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula) throws QFormulaException
{ {
@ -75,11 +79,13 @@ 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(); StringBuilder token = new StringBuilder();
List<Serializable> result = new ArrayList<>(); List<Serializable> result = new ArrayList<>();
char previousChar = 0; char previousChar = 0;
@ -92,22 +98,24 @@ public class FormulaInterpreter
char c = formula.charAt(i.getAndIncrement()); char c = formula.charAt(i.getAndIncrement());
if(c == '(' && i.get() < formula.length() - 1) 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); List<Serializable> args = interpretFormula(variableInterpreter, formula, i);
Serializable evaluate = evaluate(functionName.toString(), args, variableInterpreter); Serializable evaluate = evaluate(token.toString(), args, variableInterpreter);
result.add(evaluate); result.add(evaluate);
} }
else if(c == ')') 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. // // close paren means: end this sub-parse. evaluate the current token, //
// unless we just closed a paren. // // add it to the result list, and return the result list. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // unless we just closed a paren - then we can just return. //
//////////////////////////////////////////////////////////////////////////
if(previousChar != ')') if(previousChar != ')')
{ {
Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter); Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
result.add(evaluate); result.add(evaluate);
} }
return (result); return (result);
@ -115,22 +123,22 @@ public class FormulaInterpreter
else if(c == ',') else if(c == ',')
{ {
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
// comma means: evaluate the current thing; add it to the result list // // comma means: evaluate the current token; add it to the result list //
// unless we just closed a paren. // // unless we just closed a paren - then we can just return. //
///////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////
if(previousChar != ')') if(previousChar != ')')
{ {
Serializable evaluate = evaluate(functionName.toString(), Collections.emptyList(), variableInterpreter); Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
result.add(evaluate); result.add(evaluate);
} }
functionName = new StringBuilder(); token = new StringBuilder();
} }
else else
{ {
//////////////////////////////////////////////// /////////////////////////////////////////////////
// else, we add this char to the current name // // else, we add this char to the current token //
//////////////////////////////////////////////// /////////////////////////////////////////////////
functionName.append(c); token.append(c);
} }
} }
@ -139,9 +147,9 @@ public class FormulaInterpreter
//////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(result.isEmpty()) 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); 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(token)
switch(functionName)
{ {
case "ADD": case "ADD":
{ {
@ -209,7 +217,13 @@ public class FormulaInterpreter
} }
case "IF": 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); List<Serializable> actualArgs = getArgumentList(args, 3, variableInterpreter);
Serializable condition = actualArgs.get(0); Serializable condition = actualArgs.get(0);
boolean conditionBoolean; boolean conditionBoolean;
@ -237,7 +251,7 @@ public class FormulaInterpreter
} }
else else
{ {
conditionBoolean = StringUtils.hasContent(s); throw (new QFormulaException("Could not evaluate string '" + s + "' as a boolean."));
} }
} }
else else
@ -276,7 +290,7 @@ public class FormulaInterpreter
{ {
try try
{ {
return (ValueUtils.getValueAsBigDecimal(functionName)); return (ValueUtils.getValueAsBigDecimal(token));
} }
catch(Exception e) catch(Exception e)
{ {
@ -285,7 +299,7 @@ public class FormulaInterpreter
try try
{ {
return (variableInterpreter.interpret(functionName)); return (variableInterpreter.interpret(token));
} }
catch(Exception e) 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) 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) 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 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 private static List<Serializable> getArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
{ {
@ -374,17 +391,10 @@ public class FormulaInterpreter
List<Serializable> rs = new ArrayList<>(); List<Serializable> rs = new ArrayList<>();
for(Serializable originalArg : originalArgs) for(Serializable originalArg : originalArgs)
{
try
{ {
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null); Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
rs.add(interpretedArg); rs.add(interpretedArg);
} }
catch(QValueException e)
{
throw (new QFormulaException("Could not process [" + originalArg + "] as a number"));
}
}
return (rs); return (rs);
} }

View File

@ -73,21 +73,28 @@ import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
** Action to generate a report. ** Action to generate a report.
** **
** A report can contain 1 or more Data Sources - e.g., tables + filters that define ** 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. ** 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, ** (how do those work in non-XLSX formats??). Views can either be:
** summaries (like pivot tables, but called summary to avoid confusion with "native" ** - plain tables,
** pivot tables), or native pivot tables (not initially supported, due to lack of ** - summaries (like pivot tables, but called summary to avoid confusion with "native" pivot tables),
** support in fastexcel...). ** - native pivot tables (not initially supported, due to lack of support in fastexcel...).
*******************************************************************************/ *******************************************************************************/
public class GenerateReportAction public class GenerateReportAction
{ {
////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////
// viewName > PivotKey > fieldName > Aggregates // // summaryAggregates and varianceAggregates are multi-level maps, ala: //
////////////////////////////////////////////////// // viewName > SummaryKey > fieldName > Aggregates //
Map<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> pivotAggregates = new HashMap<>(); // e.g.: //
Map<String, Map<PivotKey, Map<String, AggregatesInterface<?>>>> variancePivotAggregates = new HashMap<>(); // 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<?>> totalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>(); Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>();
@ -120,7 +127,7 @@ public class GenerateReportAction
.filter(v -> v.getDataSourceName().equals(dataSource.getName())) .filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList(); .toList();
List<QReportView> dataSourcePivotViews = report.getViews().stream() List<QReportView> dataSourceSummaryViews = report.getViews().stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY)) .filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getDataSourceName().equals(dataSource.getName())) .filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList(); .toList();
@ -130,14 +137,15 @@ public class GenerateReportAction
.filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName())) .filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName()))
.toList(); .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(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 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 // // start the table-view (e.g., open this tab in xlsx) and then run the query-loop //
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
startTableView(reportInput, dataSource, dataSourceTableView); startTableView(reportInput, dataSource, dataSourceTableView);
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.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename()); exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(getTitle(reportView, variableInterpreter)); exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
exportInput.setIncludeHeaderRow(reportView.getHeaderRow()); exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow());
exportInput.setReportOutputStream(reportInput.getReportOutputStream()); exportInput.setReportOutputStream(reportInput.getReportOutputStream());
List<QFieldMetaData> fields; 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 // // 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(); 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()); QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
@ -364,14 +372,14 @@ public class GenerateReportAction
reportStreamer.addRecords(records); reportStreamer.addRecords(records);
} }
////////////////////////////// /////////////////////////////////
// do aggregates for pivots // // do aggregates for summaries //
////////////////////////////// /////////////////////////////////
if(pivotViews != null) 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) for(QReportView variantView : variantViews)
{ {
addRecordsToPivotAggregates(variantView, table, records, variancePivotAggregates); addRecordsToSummaryAggregates(variantView, table, records, varianceAggregates);
} }
} }
/////////////////////////////////////////// ///////////////////////////////////////////
// do totals too, if any views want them // // 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) 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) 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) for(QRecord record : records)
{ {
PivotKey key = new PivotKey(); SummaryKey key = new SummaryKey();
for(String pivotField : view.getPivotFields()) for(String summaryField : view.getPivotFields())
{ {
Serializable pivotValue = record.getValue(pivotField); Serializable summaryValue = record.getValue(summaryField);
if(table.getField(pivotField).getPossibleValueSourceName() != null) 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 // // be careful here, with these key objects, and their identity, being used as map keys //
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
PivotKey subKey = key.clone(); SummaryKey subKey = key.clone();
addRecordToPivotKeyAggregates(table, record, viewAggregates, subKey); 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<>()); Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
addRecordToAggregatesMap(table, record, keyAggregates); 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(); List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
for(QReportView view : reportViews) for(QReportView view : reportViews)
{ {
QReportDataSource dataSource = report.getDataSource(view.getDataSourceName()); QReportDataSource dataSource = report.getDataSource(view.getDataSourceName());
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable()); QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
PivotOutput pivotOutput = computePivotRowsForView(reportInput, view, table); SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
ExportInput exportInput = new ExportInput(reportInput.getInstance()); ExportInput exportInput = new ExportInput(reportInput.getInstance());
exportInput.setSession(reportInput.getSession()); exportInput.setSession(reportInput.getSession());
exportInput.setReportFormat(reportFormat); exportInput.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename()); exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(pivotOutput.titleRow); exportInput.setTitleRow(summaryOutput.titleRow);
exportInput.setIncludeHeaderRow(view.getHeaderRow()); exportInput.setIncludeHeaderRow(view.getIncludeHeaderRow());
exportInput.setReportOutputStream(reportInput.getReportOutputStream()); exportInput.setReportOutputStream(reportInput.getReportOutputStream());
reportStreamer.setDisplayFormats(getDisplayFormatMap(view)); reportStreamer.setDisplayFormats(getDisplayFormatMap(view));
reportStreamer.start(exportInput, getFields(table, view), view.getLabel()); 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(); QValueFormatter valueFormatter = new QValueFormatter();
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter(); QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues()); variableInterpreter.addValueMap("input", reportInput.getInputValues());
variableInterpreter.addValueMap("total", getPivotValuesForInterpreter(totalAggregates)); variableInterpreter.addValueMap("total", getSummaryValuesForInterpreter(totalAggregates));
/////////// ///////////
// title // // title //
/////////// ///////////
String title = getTitle(view, variableInterpreter); String title = getTitle(view, variableInterpreter);
///////////// /////////////////////////
// headers // // create summary rows //
///////////// /////////////////////////
for(String field : view.getPivotFields()) 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()); SummaryKey summaryKey = entry.getKey();
}
for(QReportField column : view.getColumns())
{
System.out.printf("%25s", column.getLabel());
}
System.out.println();
///////////////////////
// create pivot rows //
///////////////////////
List<QRecord> pivotRows = new ArrayList<>();
for(Map.Entry<PivotKey, Map<String, AggregatesInterface<?>>> entry : pivotAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
{
PivotKey pivotKey = entry.getKey();
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue(); 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<>(); HashMap<String, Serializable> thisRowValues = new HashMap<>();
variableInterpreter.addValueMap("thisRow", thisRowValues); variableInterpreter.addValueMap("thisRow", thisRowValues);
if(!variancePivotAggregates.isEmpty()) if(!varianceAggregates.isEmpty())
{ {
Map<PivotKey, Map<String, AggregatesInterface<?>>> varianceMap = variancePivotAggregates.getOrDefault(view.getName(), Collections.emptyMap()); Map<SummaryKey, Map<String, AggregatesInterface<?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(pivotKey, Collections.emptyMap()); Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceSubMap)); Map<String, Serializable> varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
variableInterpreter.addValueMap("variancePivot", varianceValues);
variableInterpreter.addValueMap("variance", varianceValues);
} }
QRecord pivotRow = new QRecord(); QRecord summaryRow = new QRecord();
pivotRows.add(pivotRow); summaryRows.add(summaryRow);
////////////////////////// ////////////////////////////
// add the pivot values // // add the summary values //
////////////////////////// ////////////////////////////
for(Pair<String, Serializable> key : pivotKey.getKeys()) 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 // // for summary subtotals, add the text "Total" to the last field in this key //
///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
if(pivotKey.getKeys().size() < view.getPivotFields().size()) if(summaryKey.getKeys().size() < view.getPivotFields().size())
{ {
String fieldName = pivotKey.getKeys().get(pivotKey.getKeys().size() - 1).getA(); String fieldName = summaryKey.getKeys().get(summaryKey.getKeys().size() - 1).getA();
pivotRow.setValue(fieldName, pivotRow.getValueString(fieldName) + " Total"); summaryRow.setValue(fieldName, summaryRow.getValueString(fieldName) + " Total");
} }
/////////////////////////// ///////////////////////////
@ -633,47 +631,29 @@ public class GenerateReportAction
for(QReportField column : view.getColumns()) for(QReportField column : view.getColumns())
{ {
Serializable serializable = getValueForColumn(variableInterpreter, column); Serializable serializable = getValueForColumn(variableInterpreter, column);
pivotRow.setValue(column.getName(), serializable); summaryRow.setValue(column.getName(), serializable);
thisRowValues.put(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())) 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 // // totals row //
//////////////// ////////////////
QRecord totalRow = null; QRecord totalRow = null;
if(view.getTotalRow()) if(view.getIncludeTotalRow())
{ {
totalRow = new QRecord(); totalRow = new QRecord();
@ -682,16 +662,17 @@ public class GenerateReportAction
if(totalRow.getValues().isEmpty()) if(totalRow.getValues().isEmpty())
{ {
totalRow.setValue(pivotField, "Totals"); totalRow.setValue(pivotField, "Totals");
System.out.printf("%-15s", "Totals");
}
else
{
System.out.printf("%-15s", "");
} }
} }
variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(totalAggregates)); Map<String, Serializable> totalValues = getSummaryValuesForInterpreter(totalAggregates);
variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceTotalAggregates)); 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<>(); HashMap<String, Serializable> thisRowValues = new HashMap<>();
variableInterpreter.addValueMap("thisRow", thisRowValues); variableInterpreter.addValueMap("thisRow", thisRowValues);
@ -702,13 +683,10 @@ public class GenerateReportAction
thisRowValues.put(column.getName(), serializable); thisRowValues.put(column.getName(), serializable);
String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable); String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable);
System.out.printf("%25s", formatted); }
} }
System.out.println(); return (new SummaryOutput(summaryRows, title, totalRow));
}
return (new PivotOutput(pivotRows, title, totalRow));
} }
@ -734,10 +712,6 @@ public class GenerateReportAction
title = view.getTitleFormat(); title = view.getTitleFormat();
} }
if(StringUtils.hasContent(title))
{
System.out.println(title);
}
return title; return title;
} }
@ -767,7 +741,7 @@ public class GenerateReportAction
** **
*******************************************************************************/ *******************************************************************************/
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
private int pivotRowComparator(QReportView view, QRecord o1, QRecord o2) private int summaryRowComparator(QReportView view, QRecord o1, QRecord o2)
{ {
if(o1 == 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()) for(Map.Entry<String, AggregatesInterface<?>> subEntry : fieldAggregates.entrySet())
{ {
String fieldName = subEntry.getKey(); String fieldName = subEntry.getKey();
AggregatesInterface<?> aggregates = subEntry.getValue(); AggregatesInterface<?> aggregates = subEntry.getValue();
pivotValuesForInterpreter.put("sum." + fieldName, aggregates.getSum()); summaryValuesForInterpreter.put("sum." + fieldName, aggregates.getSum());
pivotValuesForInterpreter.put("count." + fieldName, aggregates.getCount()); summaryValuesForInterpreter.put("count." + fieldName, aggregates.getCount());
// todo min, max, avg 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 @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"); LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
for(QRecord qRecord : qRecords) for(QRecord qRecord : qRecords)
{ {
addRecord(qRecord); 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<>(); 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; return false;
} }
PivotKey pivotKey = (PivotKey) o; SummaryKey summaryKey = (SummaryKey) o;
return Objects.equals(keys, pivotKey.keys); return Objects.equals(keys, summaryKey.keys);
} }
@ -114,9 +114,9 @@ public class PivotKey implements Cloneable
** **
*******************************************************************************/ *******************************************************************************/
@Override @Override
public PivotKey clone() public SummaryKey clone()
{ {
PivotKey clone = new PivotKey(); SummaryKey clone = new SummaryKey();
for(Pair<String, Serializable> key : keys) 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> 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 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 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 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) 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) 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) private String formatValue(String displayFormat, String fieldName, Serializable value)
{ {
@ -159,9 +160,6 @@ public class QValueFormatter
return (formatRecordLabelExceptionalCases(table, record)); return (formatRecordLabelExceptionalCases(table, record));
} }
///////////////////////////////////////////////////////////////////////
// get list of values, then pass them to the string formatter method //
///////////////////////////////////////////////////////////////////////
try try
{ {
return formatStringWithFields(table.getRecordLabelFormat(), table.getRecordLabelFields(), record.getDisplayValues(), record.getValues()); 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() List<Serializable> values = formatFields.stream()
.map(fieldName -> .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) public String formatStringWithValues(String formatString, List<String> formatValues)
{ {

View File

@ -227,14 +227,14 @@ public class QInstanceEnricher
field.setLabel(nameToLabel(field.getName())); field.setLabel(nameToLabel(field.getName()));
} }
///////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
// if this field has a possibleValueSource // // if this field has a possibleValueSource //
// and that PVS exists in the instance // // and that PVS exists in the instance //
// and it's a table-type PVS and the table name is set // // 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 it's a valid table in the instance, and the table is in some app //
// and the field doesn't have a LINK adornment // // and the field doesn't have a LINK adornment //
// then add a link-to-record-from-table adornment to the field. // // then add a link-to-record-from-table adornment to the field. //
///////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(field.getPossibleValueSourceName())) if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{ {
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());

View File

@ -149,13 +149,17 @@ public class QMetaDataVariableInterpreter
*******************************************************************************/ *******************************************************************************/
public Serializable interpretForObject(String value) 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 is null, output is null.
** If input looks like ${env.X}, then the return value is the value of the env variable 'X' ** 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("}")) if(value.startsWith(envPrefix) && value.endsWith("}"))
{ {
String envVarName = value.substring(envPrefix.length()).replaceFirst("}$", ""); String envVarName = value.substring(envPrefix.length()).replaceFirst("}$", "");
return (getEnvironmentVariable(envVarName)); String result = getEnvironmentVariable(envVarName);
return (result == null ? defaultIfLooksLikeVariableButNotFound : result);
} }
String propPrefix = "${prop."; String propPrefix = "${prop.";
if(value.startsWith(propPrefix) && value.endsWith("}")) if(value.startsWith(propPrefix) && value.endsWith("}"))
{ {
String propertyName = value.substring(propPrefix.length()).replaceFirst("}$", ""); String propertyName = value.substring(propPrefix.length()).replaceFirst("}$", "");
return (System.getProperty(propertyName)); String result = System.getProperty(propertyName);
return (result == null ? defaultIfLooksLikeVariableButNotFound : result);
} }
String literalPrefix = "${literal."; 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 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) 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 public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
{ {

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 public class QReportDataSource
{ {

View File

@ -41,9 +41,11 @@ public class QReportView implements Cloneable
private String titleFormat; private String titleFormat;
private List<String> titleFields; private List<String> titleFields;
private List<String> pivotFields; private List<String> pivotFields;
private boolean headerRow = true;
private boolean totalRow = false; private boolean includeHeaderRow = true;
private boolean pivotSubTotals = false; private boolean includeTotalRow = false;
private boolean includePivotSubTotals = false;
private List<QReportField> columns; private List<QReportField> columns;
private List<QFilterOrderBy> orderByFields; private List<QFilterOrderBy> orderByFields;
@ -332,9 +334,9 @@ public class QReportView implements Cloneable
** Getter for headerRow ** 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 ** 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 ** Fluent setter for headerRow
** **
*******************************************************************************/ *******************************************************************************/
public QReportView withHeaderRow(boolean headerRow) public QReportView withIncludeHeaderRow(boolean headerRow)
{ {
this.headerRow = headerRow; this.includeHeaderRow = headerRow;
return (this); return (this);
} }
@ -366,9 +368,9 @@ public class QReportView implements Cloneable
** Getter for totalRow ** 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 ** 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 ** Fluent setter for totalRow
** **
*******************************************************************************/ *******************************************************************************/
public QReportView withTotalRow(boolean totalRow) public QReportView withIncludeTotalRow(boolean totalRow)
{ {
this.totalRow = totalRow; this.includeTotalRow = totalRow;
return (this); return (this);
} }
@ -400,9 +402,9 @@ public class QReportView implements Cloneable
** Getter for pivotSubTotals ** 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 ** 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 ** Fluent setter for pivotSubTotals
** **
*******************************************************************************/ *******************************************************************************/
public QReportView withPivotSubTotals(boolean pivotSubTotals) public QReportView withIncludePivotSubTotals(boolean pivotSubTotals)
{ {
this.pivotSubTotals = pivotSubTotals; this.includePivotSubTotals = pivotSubTotals;
return (this); 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> 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> 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> 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("Yes", interpretFormula(vi, "IF(GT(${input.one},0),Yes,No)"));
assertEquals("No", interpretFormula(vi, "IF(LT(${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") .withDataSourceName("persons")
.withType(ReportType.SUMMARY) .withType(ReportType.SUMMARY)
.withPivotFields(List.of("lastName")) .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") .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}")) .withTitleFields(List.of("${input.startDate}", "${input.endDate}"))
.withOrderByFields(List.of(new QFilterOrderBy("shoeCount"), new QFilterOrderBy("sumPrice", false))) .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("bar", variableInterpreter.interpretForObject("${input.foo}"));
assertEquals(new BigDecimal("3.50"), variableInterpreter.interpretForObject("${input.amount}")); 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("input", Map.of("amount", new BigDecimal("3.50"), "x", "y"));
variableInterpreter.addValueMap("others", Map.of("foo", "fu", "amount", new BigDecimal("1.75"))); 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("fu", variableInterpreter.interpretForObject("${others.foo}"));
assertEquals(new BigDecimal("3.50"), variableInterpreter.interpretForObject("${input.amount}")); assertEquals(new BigDecimal("3.50"), variableInterpreter.interpretForObject("${input.amount}"));
assertEquals(new BigDecimal("1.75"), variableInterpreter.interpretForObject("${others.amount}")); assertEquals(new BigDecimal("1.75"), variableInterpreter.interpretForObject("${others.amount}"));
assertEquals("y", variableInterpreter.interpretForObject("${input.x}")); assertEquals("y", variableInterpreter.interpretForObject("${input.x}"));
assertEquals("${others.x}", variableInterpreter.interpretForObject("${others.x}")); assertNull(variableInterpreter.interpretForObject("${others.x}"));
assertEquals("${input.nil}", variableInterpreter.interpretForObject("${input.nil}")); assertNull(variableInterpreter.interpretForObject("${input.nil}"));
} }