From 3f84271a362c4f12fce588b36a084b93c38ee139 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 3 Oct 2022 09:09:06 -0500 Subject: [PATCH] Feedback from code reviews --- .../actions/reporting/CsvExportStreamer.java | 3 +- .../reporting/ExcelExportStreamer.java | 30 +- .../core/actions/reporting/ExportAction.java | 12 +- .../reporting/ExportStreamerInterface.java | 2 +- .../actions/reporting/FormulaInterpreter.java | 106 +++---- .../reporting/GenerateReportAction.java | 264 ++++++++---------- .../reporting/ListOfMapsExportStreamer.java | 4 +- .../{PivotKey.java => SummaryKey.java} | 12 +- .../customizers/ReportViewCustomizer.java | 3 +- .../BoldHeaderAndFooterExcelStyler.java | 2 +- .../excelformatting/ExcelStylerInterface.java | 3 +- .../excelformatting/PlainExcelStyler.java | 2 +- .../core/actions/values/QValueFormatter.java | 18 +- .../core/instances/QInstanceEnricher.java | 16 +- .../QMetaDataVariableInterpreter.java | 14 +- .../ProcessSummaryLineInterface.java | 5 +- .../processes/ProcessSummaryRecordLink.java | 5 +- .../metadata/reporting/QReportDataSource.java | 3 +- .../model/metadata/reporting/QReportView.java | 60 ++-- .../utils/aggregates/AggregatesInterface.java | 3 +- .../aggregates/BigDecimalAggregates.java | 2 +- .../utils/aggregates/IntegerAggregates.java | 2 +- .../reporting/FormulaInterpreterTest.java | 6 + .../reporting/GenerateReportActionTest.java | 2 +- .../QMetaDataVariableInterpreterTest.java | 8 +- 25 files changed, 303 insertions(+), 284 deletions(-) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/{PivotKey.java => SummaryKey.java} (93%) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java index d35ec31b..4b8a9a46 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/CsvExportStreamer.java @@ -118,7 +118,7 @@ public class CsvExportStreamer implements ExportStreamerInterface ** *******************************************************************************/ @Override - public int addRecords(List qRecords) throws QReportingException + public void addRecords(List 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()); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java index 103156fc..cc68d18b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExcelExportStreamer.java @@ -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 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 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 qRecords) throws QReportingException + public void addRecords(List 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)); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java index d0a7b3d8..21558cd5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportAction.java @@ -208,9 +208,9 @@ public class ExportAction lastReceivedRecordsAt = System.currentTimeMillis(); nextSleepMillis = INIT_SLEEP_MS; - List records = recordPipe.consumeAvailableRecords(); - int recordsConsumed = reportStreamer.addRecords(records); - recordCount += recordsConsumed; + List 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 records = recordPipe.consumeAvailableRecords(); - int recordsConsumed = reportStreamer.addRecords(records); - recordCount += recordsConsumed; + List records = recordPipe.consumeAvailableRecords(); + reportStreamer.addRecords(records); + recordCount += records.size(); long reportEndTime = System.currentTimeMillis(); LOG.info((countFromPreExecute != null diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java index 791c1aa1..473b3b34 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportStreamerInterface.java @@ -43,7 +43,7 @@ public interface ExportStreamerInterface /******************************************************************************* ** Called as records flow into the pipe. ******************************************************************************/ - int addRecords(List recordList) throws QReportingException; + void addRecords(List recordList) throws QReportingException; /******************************************************************************* ** Called once, after all rows are available. Meant to write a footer, or close resources, for example. diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java index 70a9221e..68d3c7f5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreter.java @@ -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 interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula, AtomicInteger i) throws QFormulaException + static List interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula, AtomicInteger i) throws QFormulaException { - StringBuilder functionName = new StringBuilder(); - List result = new ArrayList<>(); + StringBuilder token = new StringBuilder(); + List 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 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 args, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException + private static Serializable evaluate(String token, List 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 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 numbers, Supplier 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 numbers, Supplier 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 getNumberArgumentList(List 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 getArgumentList(List originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException { @@ -375,15 +392,8 @@ public class FormulaInterpreter List 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); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java index 7fd96769..76c0eb0e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportAction.java @@ -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>>> pivotAggregates = new HashMap<>(); - Map>>> 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>>> summaryAggregates = new HashMap<>(); + Map>>> varianceAggregates = new HashMap<>(); Map> totalAggregates = new HashMap<>(); Map> varianceTotalAggregates = new HashMap<>(); @@ -120,7 +127,7 @@ public class GenerateReportAction .filter(v -> v.getDataSourceName().equals(dataSource.getName())) .toList(); - List dataSourcePivotViews = report.getViews().stream() + List 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 fields; @@ -226,7 +234,7 @@ public class GenerateReportAction /******************************************************************************* ** *******************************************************************************/ - private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List pivotViews, List variantViews) throws QException + private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List summaryViews, List 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 records, QReportView tableView, List pivotViews, List variantViews) throws QException + private Integer consumeRecords(ReportInput reportInput, QReportDataSource dataSource, List records, QReportView tableView, List summaryViews, List 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 records, Map>>> aggregatesMap) + private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List records, Map>>> aggregatesMap) { - Map>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>()); + Map>> 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>> viewAggregates, PivotKey key) + private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map>> viewAggregates, SummaryKey key) { Map> 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 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 summaryRows = new ArrayList<>(); + for(Map.Entry>> 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 pivotRows = new ArrayList<>(); - for(Map.Entry>> entry : pivotAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet()) - { - PivotKey pivotKey = entry.getKey(); + SummaryKey summaryKey = entry.getKey(); Map> fieldAggregates = entry.getValue(); - variableInterpreter.addValueMap("pivot", getPivotValuesForInterpreter(fieldAggregates)); + Map summaryValues = getSummaryValuesForInterpreter(fieldAggregates); + variableInterpreter.addValueMap("pivot", summaryValues); + variableInterpreter.addValueMap("summary", summaryValues); HashMap thisRowValues = new HashMap<>(); variableInterpreter.addValueMap("thisRow", thisRowValues); - if(!variancePivotAggregates.isEmpty()) + if(!varianceAggregates.isEmpty()) { - Map>> varianceMap = variancePivotAggregates.getOrDefault(view.getName(), Collections.emptyMap()); - Map> varianceSubMap = varianceMap.getOrDefault(pivotKey, Collections.emptyMap()); - variableInterpreter.addValueMap("variancePivot", getPivotValuesForInterpreter(varianceSubMap)); + Map>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap()); + Map> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap()); + Map 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 key : pivotKey.getKeys()) + //////////////////////////// + // add the summary values // + //////////////////////////// + for(Pair 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 totalValues = getSummaryValuesForInterpreter(totalAggregates); + variableInterpreter.addValueMap("pivot", totalValues); + variableInterpreter.addValueMap("summary", totalValues); + + Map varianceTotalValues = getSummaryValuesForInterpreter(varianceTotalAggregates); + variableInterpreter.addValueMap("variancePivot", varianceTotalValues); + variableInterpreter.addValueMap("variance", varianceTotalValues); + HashMap 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 getPivotValuesForInterpreter(Map> fieldAggregates) + private Map getSummaryValuesForInterpreter(Map> fieldAggregates) { - Map pivotValuesForInterpreter = new HashMap<>(); + Map summaryValuesForInterpreter = new HashMap<>(); for(Map.Entry> 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 pivotRows, String titleRow, QRecord totalRow) + private record SummaryOutput(List summaryRows, String titleRow, QRecord totalRow) { } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java index 10781064..e1fe89b6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/ListOfMapsExportStreamer.java @@ -113,15 +113,13 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface ** *******************************************************************************/ @Override - public int addRecords(List qRecords) throws QReportingException + public void addRecords(List qRecords) throws QReportingException { LOG.info("Consuming [" + qRecords.size() + "] records from the pipe"); - for(QRecord qRecord : qRecords) { addRecord(qRecord); } - return (qRecords.size()); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/SummaryKey.java similarity index 93% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/SummaryKey.java index a12a54c0..7c7bce06 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/PivotKey.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/SummaryKey.java @@ -32,7 +32,7 @@ import com.kingsrook.qqq.backend.core.utils.Pair; /******************************************************************************* ** *******************************************************************************/ -public class PivotKey implements Cloneable +public class SummaryKey implements Cloneable { private List> 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 key : keys) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportViewCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportViewCustomizer.java index 2f5d6f92..39ddb2e8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportViewCustomizer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/ReportViewCustomizer.java @@ -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 { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java index a42cd573..12dc6685 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/BoldHeaderAndFooterExcelStyler.java @@ -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 { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java index e03855c4..ed58aba3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/ExcelStylerInterface.java @@ -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 { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java index a47b7564..3fdd189a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/excelformatting/PlainExcelStyler.java @@ -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 { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java index 5d5234d7..50ca7d04 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QValueFormatter.java @@ -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 formatFields, Map displayValueMap, Map rawValueMap) + private String formatStringWithFields(String formatString, List formatFields, Map displayValueMap, Map rawValueMap) { List 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 formatValues) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 3e823bee..39399433 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -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()); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java index 202c2687..2e9421c3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java @@ -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."; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLineInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLineInterface.java index 044f0475..73f520ae 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLineInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryLineInterface.java @@ -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) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java index b9ee8ab2..2eb7a905 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessSummaryRecordLink.java @@ -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); } - + /******************************************************************************* diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java index af1a0c40..f0c96b5e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportDataSource.java @@ -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 { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java index dad6d3cc..a486f0df 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportView.java @@ -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 titleFields; - private List 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 titleFields; + private List pivotFields; + + private boolean includeHeaderRow = true; + private boolean includeTotalRow = false; + private boolean includePivotSubTotals = false; + private List columns; private List 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); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesInterface.java index 9bfe8d0a..074c2469 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/AggregatesInterface.java @@ -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 { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/BigDecimalAggregates.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/BigDecimalAggregates.java index ae66baf8..da7f1703 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/BigDecimalAggregates.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/BigDecimalAggregates.java @@ -26,7 +26,7 @@ import java.math.BigDecimal; /******************************************************************************* - ** + ** BigDecimal version of data aggregator *******************************************************************************/ public class BigDecimalAggregates implements AggregatesInterface { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/IntegerAggregates.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/IntegerAggregates.java index c193f72a..292e8a01 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/IntegerAggregates.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/aggregates/IntegerAggregates.java @@ -26,7 +26,7 @@ import java.math.BigDecimal; /******************************************************************************* - ** + ** Integer version of data aggregator *******************************************************************************/ public class IntegerAggregates implements AggregatesInterface { diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java index 62bafe5b..d87c630b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/FormulaInterpreterTest.java @@ -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."); } } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java index cce7bfbb..4d8ea638 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/GenerateReportActionTest.java @@ -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))) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java index 3255bcec..741090c1 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java @@ -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}")); }