From 799b695e14b49c1701016473ac2f965cd1d0e7a0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 21 Dec 2022 11:37:16 -0600 Subject: [PATCH] Checkpoint on report and export changes, possible value translating --- .../RecordAutomationStatusUpdater.java | 6 + .../actions/reporting/BufferedRecordPipe.java | 89 +++++++++++++ .../reporting/ExcelExportStreamer.java | 8 -- .../core/actions/reporting/ExportAction.java | 64 +++++++-- .../reporting/GenerateReportAction.java | 78 +++++++---- .../actions/reporting/JsonExportStreamer.java | 31 ++++- .../DataSourceQueryInputCustomizer.java | 45 +++++++ .../core/actions/tables/QueryAction.java | 12 +- .../values/QPossibleValueTranslator.java | 126 +++++++++++++++--- .../core/instances/QInstanceValidator.java | 87 +++++++++--- .../metadata/reporting/QReportDataSource.java | 35 +++++ .../metadata/reporting/QReportField.java | 72 ++++++++++ .../qqq/backend/core/utils/StringUtils.java | 44 +++++- .../values/QPossibleValueTranslatorTest.java | 2 +- .../instances/QInstanceValidatorTest.java | 64 +++++++++ .../backend/core/utils/StringUtilsTest.java | 38 ++++++ .../qqq/backend/core/utils/TestUtils.java | 25 ++++ .../javalin/QJavalinImplementation.java | 38 +++++- 18 files changed, 773 insertions(+), 91 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/BufferedRecordPipe.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/DataSourceQueryInputCustomizer.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java index 22035129..9028fc6d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/automation/RecordAutomationStatusUpdater.java @@ -92,6 +92,12 @@ public class RecordAutomationStatusUpdater { for(QRecord record : records) { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // todo - seems like there's some case here, where if an order was in PENDING_INSERT, but then some other job updated the record, that we'd // + // lose that pending status, which would be a Bad Thing™... // + // problem is - we may not have the full record in here, so we can't necessarily check the record to see what status it's currently in... // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId()); // todo - another field - for the automation timestamp?? } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/BufferedRecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/BufferedRecordPipe.java new file mode 100644 index 00000000..811827c8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/BufferedRecordPipe.java @@ -0,0 +1,89 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.reporting; + + +import java.util.ArrayList; +import java.util.List; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** Subclass of RecordPipe, which uses a buffer in the addRecord method, to avoid + ** sending single-records at a time through postRecordActions and to consumers. + *******************************************************************************/ +public class BufferedRecordPipe extends RecordPipe +{ + private List buffer = new ArrayList<>(); + private Integer bufferSize = 100; + + + + /******************************************************************************* + ** Constructor - uses default buffer size + ** + *******************************************************************************/ + public BufferedRecordPipe() + { + } + + + + /******************************************************************************* + ** Constructor - customize buffer size. + ** + *******************************************************************************/ + public BufferedRecordPipe(Integer bufferSize) + { + this.bufferSize = bufferSize; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void addRecord(QRecord record) + { + buffer.add(record); + if(buffer.size() >= bufferSize) + { + addRecords(buffer); + buffer.clear(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void finalFlush() + { + if(!buffer.isEmpty()) + { + addRecords(buffer); + buffer.clear(); + } + } +} 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 66b6c2ec..6520f6b8 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 @@ -247,14 +247,6 @@ public class ExcelExportStreamer implements ExportStreamerInterface for(QFieldMetaData field : fields) { Serializable value = qRecord.getValue(field.getName()); - if(field.getPossibleValueSourceName() != null) - { - String displayValue = qRecord.getDisplayValue(field.getName()); - if(displayValue != null) - { - value = displayValue; - } - } if(value != null) { 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 21558cd5..98187f79 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 @@ -31,7 +31,7 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface; -import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QReportingException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; @@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; @@ -147,17 +148,18 @@ public class ExportAction ////////////////////////// // set up a query input // ////////////////////////// - QueryInterface queryInterface = backendModule.getQueryInterface(); - QueryInput queryInput = new QueryInput(exportInput.getInstance()); + QueryAction queryAction = new QueryAction(); + QueryInput queryInput = new QueryInput(exportInput.getInstance()); queryInput.setSession(exportInput.getSession()); queryInput.setTableName(exportInput.getTableName()); queryInput.setFilter(exportInput.getQueryFilter()); queryInput.setLimit(exportInput.getLimit()); + queryInput.setShouldTranslatePossibleValues(true); ///////////////////////////////////////////////////////////////// // tell this query that it needs to put its output into a pipe // ///////////////////////////////////////////////////////////////// - RecordPipe recordPipe = new RecordPipe(); + RecordPipe recordPipe = new BufferedRecordPipe(500); queryInput.setRecordPipe(recordPipe); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -165,13 +167,14 @@ public class ExportAction //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ReportFormat reportFormat = exportInput.getReportFormat(); ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer(); - reportStreamer.start(exportInput, getFields(exportInput), "Sheet 1"); + List fields = getFields(exportInput); + reportStreamer.start(exportInput, fields, "Sheet 1"); ////////////////////////////////////////// // run the query action as an async job // ////////////////////////////////////////// AsyncJobManager asyncJobManager = new AsyncJobManager(); - String queryJobUUID = asyncJobManager.startJob("ReportAction>QueryAction", (status) -> (queryInterface.execute(queryInput))); + String queryJobUUID = asyncJobManager.startJob("ReportAction>QueryAction", (status) -> (queryAction.execute(queryInput))); LOG.info("Started query job [" + queryJobUUID + "] for report"); AsyncJobState queryJobState = AsyncJobState.RUNNING; @@ -209,7 +212,7 @@ public class ExportAction nextSleepMillis = INIT_SLEEP_MS; List records = recordPipe.consumeAvailableRecords(); - reportStreamer.addRecords(records); + processRecords(reportStreamer, fields, records); recordCount += records.size(); LOG.info(countFromPreExecute != null @@ -238,7 +241,7 @@ public class ExportAction // send the final records to the report streamer // /////////////////////////////////////////////////// List records = recordPipe.consumeAvailableRecords(); - reportStreamer.addRecords(records); + processRecords(reportStreamer, fields, records); recordCount += records.size(); long reportEndTime = System.currentTimeMillis(); @@ -269,20 +272,59 @@ public class ExportAction + /******************************************************************************* + ** + *******************************************************************************/ + private static void processRecords(ExportStreamerInterface reportStreamer, List fields, List records) throws QReportingException + { + for(QFieldMetaData field : fields) + { + if(field.getName().endsWith(":possibleValueLabel")) + { + String effectiveFieldName = field.getName().replace(":possibleValueLabel", ""); + for(QRecord record : records) + { + String displayValue = record.getDisplayValue(effectiveFieldName); + record.setValue(field.getName(), displayValue); + } + } + } + + reportStreamer.addRecords(records); + } + + + /******************************************************************************* ** *******************************************************************************/ private List getFields(ExportInput exportInput) { - QTableMetaData table = exportInput.getTable(); + List fieldList; + QTableMetaData table = exportInput.getTable(); if(exportInput.getFieldNames() != null) { - return (exportInput.getFieldNames().stream().map(table::getField).toList()); + fieldList = exportInput.getFieldNames().stream().map(table::getField).toList(); } else { - return (new ArrayList<>(table.getFields().values())); + fieldList = new ArrayList<>(table.getFields().values()); } + + ////////////////////////////////////////// + // add fields for possible value labels // + ////////////////////////////////////////// + List returnList = new ArrayList<>(); + for(QFieldMetaData field : fieldList) + { + returnList.add(field); + if(StringUtils.hasContent(field.getPossibleValueSourceName())) + { + returnList.add(new QFieldMetaData(field.getName() + ":possibleValueLabel", QFieldType.STRING).withLabel(field.getLabel() + " Name")); + } + } + + return (returnList); } 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 db426831..e0cd8d21 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 @@ -29,11 +29,13 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.reporting.customizers.DataSourceQueryInputCustomizer; import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; @@ -68,6 +70,8 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface; import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates; import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -84,6 +88,8 @@ import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates; *******************************************************************************/ public class GenerateReportAction { + private static final Logger LOG = LogManager.getLogger(GenerateReportAction.class); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // summaryAggregates and varianceAggregates are multi-level maps, ala: // // viewName > SummaryKey > fieldName > Aggregates // @@ -214,38 +220,32 @@ public class GenerateReportAction JoinsContext joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins()); - List fields; - if(CollectionUtils.nullSafeHasContents(reportView.getColumns())) + List fields = new ArrayList<>(); + for(QReportField column : reportView.getColumns()) { - fields = new ArrayList<>(); - for(QReportField column : reportView.getColumns()) + if(column.getIsVirtual()) { - if(column.getIsVirtual()) + fields.add(column.toField()); + } + else + { + String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName()); + JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(effectiveFieldName); + if(fieldAndTableNameOrAlias.field() == null) { - fields.add(column.toField()); + throw new QReportingException("Could not find field named [" + effectiveFieldName + "] on table [" + table.getName() + "]"); } - else - { - JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(column.getName()); - if(fieldAndTableNameOrAlias.field() == null) - { - throw new QReportingException("Could not find field named [" + column.getName() + "] on table [" + table.getName() + "]"); - } - QFieldMetaData field = fieldAndTableNameOrAlias.field().clone(); - field.setName(column.getName()); - if(StringUtils.hasContent(column.getLabel())) - { - field.setLabel(column.getLabel()); - } - fields.add(field); + QFieldMetaData field = fieldAndTableNameOrAlias.field().clone(); + field.setName(column.getName()); + if(StringUtils.hasContent(column.getLabel())) + { + field.setLabel(column.getLabel()); } + fields.add(field); } } - else - { - fields = new ArrayList<>(table.getFields().values()); - } + reportStreamer.setDisplayFormats(getDisplayFormatMap(fields)); reportStreamer.start(exportInput, fields, reportView.getLabel()); } @@ -286,7 +286,7 @@ public class GenerateReportAction ///////////////////////////////////////////////////////////////// // run a record pipe loop, over the query for this data source // ///////////////////////////////////////////////////////////////// - RecordPipe recordPipe = new RecordPipe(); + RecordPipe recordPipe = new BufferedRecordPipe(1000); new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) -> { if(dataSource.getSourceTable() != null) @@ -301,6 +301,13 @@ public class GenerateReportAction queryInput.setFilter(queryFilter); queryInput.setQueryJoins(dataSource.getQueryJoins()); queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this? + + if(dataSource.getQueryInputCustomizer() != null) + { + DataSourceQueryInputCustomizer queryInputCustomizer = QCodeLoader.getAdHoc(DataSourceQueryInputCustomizer.class, dataSource.getQueryInputCustomizer()); + queryInput = queryInputCustomizer.run(reportInput, queryInput); + } + return (new QueryAction().execute(queryInput)); } else if(dataSource.getStaticDataSupplier() != null) @@ -368,8 +375,9 @@ public class GenerateReportAction for(Serializable value : criterion.getValues()) { - String valueAsString = ValueUtils.getValueAsString(value); - Serializable interpretedValue = variableInterpreter.interpret(valueAsString); + String valueAsString = ValueUtils.getValueAsString(value); + // Serializable interpretedValue = variableInterpreter.interpret(valueAsString); + Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString); newValues.add(interpretedValue); } criterion.setValues(newValues); @@ -391,6 +399,22 @@ public class GenerateReportAction //////////////////////////////////////////////////////////////////////////// if(tableView != null) { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if any fields are 'showPossibleValueLabel', then move display values for them into the record's values map // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + for(QReportField column : tableView.getColumns()) + { + if(column.getShowPossibleValueLabel()) + { + String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName()); + for(QRecord record : records) + { + String displayValue = record.getDisplayValue(effectiveFieldName); + record.setValue(column.getName(), displayValue); + } + } + } + reportStreamer.addRecords(records); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/JsonExportStreamer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/JsonExportStreamer.java index c58f2930..90d76f2d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/JsonExportStreamer.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/JsonExportStreamer.java @@ -24,14 +24,18 @@ package com.kingsrook.qqq.backend.core.actions.reporting; import java.io.IOException; import java.io.OutputStream; +import java.io.Serializable; import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QReportingException; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -49,6 +53,7 @@ public class JsonExportStreamer implements ExportStreamerInterface private OutputStream outputStream; private boolean needComma = false; + private boolean prettyPrint = true; @@ -74,7 +79,7 @@ public class JsonExportStreamer implements ExportStreamerInterface try { - outputStream.write("[".getBytes(StandardCharsets.UTF_8)); + outputStream.write('['); } catch(IOException e) { @@ -109,11 +114,25 @@ public class JsonExportStreamer implements ExportStreamerInterface { if(needComma) { - outputStream.write(",".getBytes(StandardCharsets.UTF_8)); + outputStream.write(','); + } + + Map mapForJson = new LinkedHashMap<>(); + for(QFieldMetaData field : fields) + { + String labelForJson = StringUtils.lcFirst(field.getLabel().replace(" ", "")); + mapForJson.put(labelForJson, qRecord.getValue(field.getName())); + } + + String json = prettyPrint ? JsonUtils.toPrettyJson(mapForJson) : JsonUtils.toJson(mapForJson); + + if(prettyPrint) + { + outputStream.write('\n'); } - String json = JsonUtils.toJson(qRecord); outputStream.write(json.getBytes(StandardCharsets.UTF_8)); + outputStream.flush(); // todo - less often? needComma = true; } @@ -144,7 +163,11 @@ public class JsonExportStreamer implements ExportStreamerInterface { try { - outputStream.write("]".getBytes(StandardCharsets.UTF_8)); + if(prettyPrint) + { + outputStream.write('\n'); + } + outputStream.write(']'); } catch(IOException e) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/DataSourceQueryInputCustomizer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/DataSourceQueryInputCustomizer.java new file mode 100644 index 00000000..a20c7997 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/customizers/DataSourceQueryInputCustomizer.java @@ -0,0 +1,45 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.reporting.customizers; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; + + +/******************************************************************************* + ** Interface for customizer on a QReportDataSource's query. + ** + ** Useful, for example, to look at what input field values were given, and change + ** the query filter (e.g., conditional criteria), or issue an error based on the + ** combination of input fields given. + *******************************************************************************/ +public interface DataSourceQueryInputCustomizer +{ + + /******************************************************************************* + ** + *******************************************************************************/ + QueryInput run(ReportInput reportInput, QueryInput queryInput) throws QException; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index e757a4ae..a63de9b9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -36,6 +37,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -44,6 +47,8 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; *******************************************************************************/ public class QueryAction { + private static final Logger LOG = LogManager.getLogger(QueryAction.class); + private Optional postQueryRecordCustomizer; private QueryInput queryInput; @@ -72,6 +77,11 @@ public class QueryAction QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput); // todo post-customization - can do whatever w/ the result if you want + if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe) + { + bufferedRecordPipe.finalFlush(); + } + if(queryInput.getRecordPipe() == null) { postRecordActions(queryOutput.getRecords()); @@ -100,7 +110,7 @@ public class QueryAction { qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession()); } - qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records); + qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records, queryInput.getQueryJoins()); } if(queryInput.getShouldGenerateDisplayValues()) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java index 72730b05..b2728052 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.values; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -38,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperat import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -50,6 +52,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ListingHash; +import com.kingsrook.qqq.backend.core.utils.Pair; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -91,13 +95,24 @@ public class QPossibleValueTranslator ** For a list of records, translate their possible values (populating their display values) *******************************************************************************/ public void translatePossibleValuesInRecords(QTableMetaData table, List records) + { + translatePossibleValuesInRecords(table, records, Collections.emptyList()); + } + + + + /******************************************************************************* + ** For a list of records, translate their possible values (populating their display values) + *******************************************************************************/ + public void translatePossibleValuesInRecords(QTableMetaData table, List records, List queryJoins) { if(records == null || table == null) { return; } - primePvsCache(table, records); + LOG.debug("Translating possible values in [" + records.size() + "] records from the [" + table.getName() + "] table."); + primePvsCache(table, records, queryJoins); for(QRecord record : records) { @@ -108,6 +123,42 @@ public class QPossibleValueTranslator record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName()))); } } + + for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins)) + { + if(queryJoin.getSelect()) + { + try + { + //////////////////////////////////////////// + // todo - aliases aren't be handled right // + //////////////////////////////////////////// + QTableMetaData joinTable = qInstance.getTable(queryJoin.getRightTable()); + for(QFieldMetaData field : joinTable.getFields().values()) + { + if(field.getPossibleValueSourceName() != null) + { + /////////////////////////////////////////////// + // avoid circling-back upon the source table // + /////////////////////////////////////////////// + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); + if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()) && table.getName().equals(possibleValueSource.getTableName())) + { + continue; + } + + String joinFieldName = joinTable.getName() + "." + field.getName(); + record.setDisplayValue(joinFieldName, translatePossibleValue(field, record.getValue(joinFieldName))); + } + } + } + catch(Exception e) + { + LOG.warn("Error translating join table possible values", e); + } + } + } + } } @@ -244,8 +295,7 @@ public class QPossibleValueTranslator ////////////////////////////////////////////////////////////// // look for cached value - if it's missing, call the primer // ////////////////////////////////////////////////////////////// - possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); - Map cacheForPvs = possibleValueCache.get(possibleValueSource.getName()); + Map cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>()); if(!cacheForPvs.containsKey(value)) { primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), List.of(value)); @@ -329,21 +379,29 @@ public class QPossibleValueTranslator /******************************************************************************* ** prime the cache (e.g., by doing bulk-queries) for table-based PVS's - ** - ** @param table the table that the records are from + ** @param table the table that the records are from ** @param records the records that have the possible value id's (e.g., foreign keys) + * @param queryJoins *******************************************************************************/ - void primePvsCache(QTableMetaData table, List records) + void primePvsCache(QTableMetaData table, List records, List queryJoins) { - ListingHash fieldsByPvsTable = new ListingHash<>(); - ListingHash pvsesByTable = new ListingHash<>(); - for(QFieldMetaData field : table.getFields().values()) + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this is a map of String(tableName - the PVS table) to Pair(String (either "" for main table in a query, or join-table + "."), field (from the table being selected from)) // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ListingHash> fieldsByPvsTable = new ListingHash<>(); + + /////////////////////////////////////////////////////////////////////////////////////// + // this is a map of String(tableName - the PVS table) to PossibleValueSource objects // + /////////////////////////////////////////////////////////////////////////////////////// + ListingHash pvsesByTable = new ListingHash<>(); + + primePvsCacheTableListingHashLoader(table, fieldsByPvsTable, pvsesByTable, ""); + for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins)) { - QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); - if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) + if(queryJoin.getSelect()) { - fieldsByPvsTable.add(possibleValueSource.getTableName(), field); - pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource); + // todo - aliases probably not handled right + primePvsCacheTableListingHashLoader(qInstance.getTable(queryJoin.getRightTable()), fieldsByPvsTable, pvsesByTable, queryJoin.getRightTable() + "."); } } @@ -352,16 +410,24 @@ public class QPossibleValueTranslator Set values = new HashSet<>(); for(QRecord record : records) { - for(QFieldMetaData field : fieldsByPvsTable.get(tableName)) + for(Pair fieldPair : fieldsByPvsTable.get(tableName)) { - Serializable fieldValue = record.getValue(field.getName()); + String fieldName = fieldPair.getA() + fieldPair.getB().getName(); + Serializable fieldValue = record.getValue(fieldName); + + ///////////////////////////////////////// + // ignore null and empty-string values // + ///////////////////////////////////////// + if(!StringUtils.hasContent(ValueUtils.getValueAsString(fieldValue))) + { + continue; + } ////////////////////////////////////// // check if value is already cached // ////////////////////////////////////// QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0); - possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); - Map cacheForPvs = possibleValueCache.get(possibleValueSource.getName()); + Map cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>()); if(!cacheForPvs.containsKey(fieldValue)) { @@ -379,6 +445,28 @@ public class QPossibleValueTranslator + /******************************************************************************* + ** Helper for the primePvsCache method + *******************************************************************************/ + private void primePvsCacheTableListingHashLoader(QTableMetaData table, ListingHash> fieldsByPvsTable, ListingHash pvsesByTable, String fieldNamePrefix) + { + for(QFieldMetaData field : table.getFields().values()) + { + QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); + if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE)) + { + fieldsByPvsTable.add(possibleValueSource.getTableName(), Pair.of(fieldNamePrefix, field)); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // todo - optimization we can put the same PVS in this listing hash multiple times... either check for dupes, or change to a set, or something smarter. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource); + } + } + } + + + /******************************************************************************* ** For a given table, and a list of pkey-values in that table, AND a list of ** possible value sources based on that table (maybe usually 1, but could be more, @@ -408,10 +496,12 @@ public class QPossibleValueTranslator ///////////////////////////////////////////////////////////////////////////////////////// if(notTooDeep()) { - queryInput.setShouldTranslatePossibleValues(true); + // todo not commit... + // queryInput.setShouldTranslatePossibleValues(true); queryInput.setShouldGenerateDisplayValues(true); } + LOG.debug("Priming PVS cache for [" + page.size() + "] ids from [" + tableName + "] table."); QueryOutput queryOutput = new QueryAction().execute(queryInput); /////////////////////////////////////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 4e2271d0..b77c2519 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; @@ -58,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; @@ -984,7 +986,7 @@ public class QInstanceValidator { if(dataSource.getQueryFilter() != null) { - validateQueryFilter("In " + dataSourceErrorPrefix + "query filter - ", qInstance.getTable(dataSource.getSourceTable()), dataSource.getQueryFilter()); + validateQueryFilter(qInstance, "In " + dataSourceErrorPrefix + "query filter - ", qInstance.getTable(dataSource.getSourceTable()), dataSource.getQueryFilter(), dataSource.getQueryJoins()); } } } @@ -999,9 +1001,9 @@ public class QInstanceValidator } } - //////////////////////////////////////// - // validate dataSources in the report // - //////////////////////////////////////// + ////////////////////////////////// + // validate views in the report // + ////////////////////////////////// if(assertCondition(CollectionUtils.nullSafeHasContents(report.getViews()), "At least 1 view must be defined in report " + reportName + ".")) { int index = 0; @@ -1015,19 +1017,30 @@ public class QInstanceValidator usedViewNames.add(view.getName()); String viewErrorPrefix = "Report " + reportName + " view " + view.getName() + " "; - assertCondition(view.getType() != null, viewErrorPrefix + " is missing its type."); - if(assertCondition(StringUtils.hasContent(view.getDataSourceName()), viewErrorPrefix + " is missing a dataSourceName")) + assertCondition(view.getType() != null, viewErrorPrefix + "is missing its type."); + if(assertCondition(StringUtils.hasContent(view.getDataSourceName()), viewErrorPrefix + "is missing a dataSourceName")) { - assertCondition(usedDataSourceNames.contains(view.getDataSourceName()), viewErrorPrefix + " has an unrecognized dataSourceName: " + view.getDataSourceName()); + assertCondition(usedDataSourceNames.contains(view.getDataSourceName()), viewErrorPrefix + "has an unrecognized dataSourceName: " + view.getDataSourceName()); } if(StringUtils.hasContent(view.getVarianceDataSourceName())) { - assertCondition(usedDataSourceNames.contains(view.getVarianceDataSourceName()), viewErrorPrefix + " has an unrecognized varianceDataSourceName: " + view.getVarianceDataSourceName()); + assertCondition(usedDataSourceNames.contains(view.getVarianceDataSourceName()), viewErrorPrefix + "has an unrecognized varianceDataSourceName: " + view.getVarianceDataSourceName()); } - // actually, this is okay if there's a customizer, so... - assertCondition(CollectionUtils.nullSafeHasContents(view.getColumns()), viewErrorPrefix + " does not have any columns."); + boolean hasColumns = CollectionUtils.nullSafeHasContents(view.getColumns()); + boolean hasViewCustomizer = view.getViewCustomizer() != null; + assertCondition(hasColumns || hasViewCustomizer, viewErrorPrefix + "does not have any columns or a view customizer."); + + Set usedColumnNames = new HashSet<>(); + for(QReportField column : CollectionUtils.nonNullList(view.getColumns())) + { + assertCondition(StringUtils.hasContent(column.getName()), viewErrorPrefix + "has a column with no name."); + assertCondition(!usedColumnNames.contains(column.getName()), viewErrorPrefix + "has multiple columns named: " + column.getName()); + usedColumnNames.add(column.getName()); + + // todo - is field name valid? + } // todo - all these too... // view.getPivotFields(); @@ -1047,34 +1060,74 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private void validateQueryFilter(String context, QTableMetaData table, QQueryFilter queryFilter) + private void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List queryJoins) { for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria())) { - if(assertCondition(StringUtils.hasContent(criterion.getFieldName()), context + "Missing fieldName for a criteria")) + String fieldName = criterion.getFieldName(); + if(assertCondition(StringUtils.hasContent(fieldName), context + "Missing fieldName for a criteria")) { - assertNoException(() -> table.getField(criterion.getFieldName()), context + "Criteria fieldName " + criterion.getFieldName() + " is not a field in this table."); + assertCondition(findField(qInstance, table, queryJoins, fieldName), context + "Criteria fieldName " + fieldName + " is not a field in this table (or in any given joins)."); } - assertCondition(criterion.getOperator() != null, context + "Missing operator for a criteria on fieldName " + criterion.getFieldName()); - assertCondition(criterion.getValues() != null, context + "Missing values for a criteria on fieldName " + criterion.getFieldName()); // todo - what about ops w/ no value (BLANK) + assertCondition(criterion.getOperator() != null, context + "Missing operator for a criteria on fieldName " + fieldName); + assertCondition(criterion.getValues() != null, context + "Missing values for a criteria on fieldName " + fieldName); // todo - what about ops w/ no value (BLANK) } for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys())) { if(assertCondition(StringUtils.hasContent(orderBy.getFieldName()), context + "Missing fieldName for an orderBy")) { - assertNoException(() -> table.getField(orderBy.getFieldName()), context + "OrderBy fieldName " + orderBy.getFieldName() + " is not a field in this table."); + assertCondition(findField(qInstance, table, queryJoins, orderBy.getFieldName()), context + "OrderBy fieldName " + orderBy.getFieldName() + " is not a field in this table (or in any given joins)."); } } for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters())) { - validateQueryFilter(context, table, subFilter); + validateQueryFilter(qInstance, context, table, subFilter, queryJoins); } } + /******************************************************************************* + ** Look for a field name in either a table, or the tables referenced in a list of query joins. + *******************************************************************************/ + private static boolean findField(QInstance qInstance, QTableMetaData table, List queryJoins, String fieldName) + { + boolean foundField = false; + try + { + table.getField(fieldName); + foundField = true; + } + catch(Exception e) + { + if(fieldName.contains(".")) + { + String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1); + for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins)) + { + QTableMetaData joinTable = qInstance.getTable(queryJoin.getRightTable()); + if(joinTable != null) + { + try + { + joinTable.getField(fieldNameAfterDot); + foundField = true; + } + catch(Exception e2) + { + continue; + } + } + } + } + } + return foundField; + } + + + /******************************************************************************* ** *******************************************************************************/ 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 260e51bb..2473e921 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 @@ -42,6 +42,7 @@ public class QReportDataSource private List queryJoins = null; + private QCodeReference queryInputCustomizer; private QCodeReference staticDataSupplier; @@ -230,4 +231,38 @@ public class QReportDataSource return (this); } + + + /******************************************************************************* + ** Getter for queryInputCustomizer + ** + *******************************************************************************/ + public QCodeReference getQueryInputCustomizer() + { + return queryInputCustomizer; + } + + + + /******************************************************************************* + ** Setter for queryInputCustomizer + ** + *******************************************************************************/ + public void setQueryInputCustomizer(QCodeReference queryInputCustomizer) + { + this.queryInputCustomizer = queryInputCustomizer; + } + + + + /******************************************************************************* + ** Fluent setter for queryInputCustomizer + ** + *******************************************************************************/ + public QReportDataSource withQueryInputCustomizer(QCodeReference queryInputCustomizer) + { + this.queryInputCustomizer = queryInputCustomizer; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java index ab8885c7..e588b511 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportField.java @@ -43,6 +43,9 @@ public class QReportField private boolean isVirtual = false; + private boolean showPossibleValueLabel = false; + private String sourceFieldName; + /******************************************************************************* @@ -281,4 +284,73 @@ public class QReportField this.isVirtual = isVirtual; return (this); } + + + + /******************************************************************************* + ** Getter for sourceFieldName + ** + *******************************************************************************/ + public String getSourceFieldName() + { + return sourceFieldName; + } + + + + /******************************************************************************* + ** Setter for sourceFieldName + ** + *******************************************************************************/ + public void setSourceFieldName(String sourceFieldName) + { + this.sourceFieldName = sourceFieldName; + } + + + + /******************************************************************************* + ** Fluent setter for sourceFieldName + ** + *******************************************************************************/ + public QReportField withSourceFieldName(String sourceFieldName) + { + this.sourceFieldName = sourceFieldName; + return (this); + } + + + + /******************************************************************************* + ** Getter for showPossibleValueLabel + ** + *******************************************************************************/ + public boolean getShowPossibleValueLabel() + { + return showPossibleValueLabel; + } + + + + /******************************************************************************* + ** Setter for showPossibleValueLabel + ** + *******************************************************************************/ + public void setShowPossibleValueLabel(boolean showPossibleValueLabel) + { + this.showPossibleValueLabel = showPossibleValueLabel; + } + + + + /******************************************************************************* + ** Fluent setter for showPossibleValueLabel + ** + *******************************************************************************/ + public QReportField withShowPossibleValueLabel(boolean showPossibleValueLabel) + { + this.showPossibleValueLabel = showPossibleValueLabel; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java index 2ee8e59e..0fc8115d 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java @@ -241,8 +241,8 @@ public class StringUtils return (null); } - StringBuilder rs = new StringBuilder(); - int size = input.size(); + StringBuilder rs = new StringBuilder(); + int size = input.size(); for(int i = 0; i < size; i++) { @@ -361,4 +361,44 @@ public class StringUtils return (size != null && size.equals(1) ? ifOne : ifNotOne); } + + + /******************************************************************************* + ** Lowercase the first char of a string. + *******************************************************************************/ + public static String lcFirst(String s) + { + if(s == null) + { + return (null); + } + + if(s.length() <= 1) + { + return (s.toLowerCase()); + } + + return (s.substring(0, 1).toLowerCase() + s.substring(1)); + } + + + + /******************************************************************************* + ** Uppercase the first char of a string. + *******************************************************************************/ + public static String ucFirst(String s) + { + if(s == null) + { + return (null); + } + + if(s.length() <= 1) + { + return (s.toUpperCase()); + } + + return (s.substring(0, 1).toUpperCase() + s.substring(1)); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java index ba66dcbb..d1f98e40 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslatorTest.java @@ -188,7 +188,7 @@ public class QPossibleValueTranslatorTest ); QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); MemoryRecordStore.resetStatistics(); - possibleValueTranslator.primePvsCache(personTable, personRecords); + possibleValueTranslator.primePvsCache(personTable, personRecords, null); // todo - test non-null queryJoins assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query"); possibleValueTranslator.translatePossibleValue(shapeField, 1); possibleValueTranslator.translatePossibleValue(shapeField, 2); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 14e1d2c8..83a1f3f3 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -55,6 +55,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -1436,6 +1437,69 @@ class QInstanceValidatorTest dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class, null)); }, "is not of the expected type"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportViewBasics() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).setViews(null), + "At least 1 view must be defined in report"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).setViews(new ArrayList<>()), + "At least 1 view must be defined in report"); + + ///////////////////////////////////////////////////////////////////////// + // meh, enricher sets a default name, so, can't easily catch this one. // + ///////////////////////////////////////////////////////////////////////// + // assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).setName(null), + // "Missing name for a view"); + // assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).setName(""), + // "Missing name for a view"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).setType(null), + "missing its type"); + + ///////////////////////////////////////////////////////////////////////// + // meh, enricher sets a default name, so, can't easily catch this one. // + ///////////////////////////////////////////////////////////////////////// + // assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).setDataSourceName(null), + // "missing a dataSourceName"); + // assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).setDataSourceName(""), + // "missing a dataSourceName"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).setDataSourceName("notADataSource"), + "has an unrecognized dataSourceName"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportViewColumns() + { + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).setColumns(null), + "does not have any columns or a view customizer"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).getColumns().get(0).setName(null), + "has a column with no name"); + + assertValidationFailureReasons((qInstance) -> qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).getColumns().get(0).setName(""), + "has a column with no name"); + + assertValidationFailureReasons((qInstance) -> + { + List columns = qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON).getViews().get(0).getColumns(); + columns.get(0).setName("id"); + columns.get(1).setName("id"); + }, + "has multiple columns named: id"); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java index 59d63d0d..3e75eb8d 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java @@ -182,6 +182,7 @@ class StringUtilsTest } + /******************************************************************************* ** *******************************************************************************/ @@ -246,4 +247,41 @@ class StringUtilsTest assertEquals("", StringUtils.plural(1, "", "es")); assertEquals("es", StringUtils.plural(2, "", "es")); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testLcFirst() + { + assertNull(StringUtils.lcFirst(null)); + assertEquals("", StringUtils.lcFirst("")); + assertEquals(" ", StringUtils.lcFirst(" ")); + assertEquals("a", StringUtils.lcFirst("A")); + assertEquals("1", StringUtils.lcFirst("1")); + assertEquals("a", StringUtils.lcFirst("a")); + assertEquals("aB", StringUtils.lcFirst("AB")); + assertEquals("aBc", StringUtils.lcFirst("ABc")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUcFirst() + { + assertNull(StringUtils.ucFirst(null)); + assertEquals("", StringUtils.ucFirst("")); + assertEquals(" ", StringUtils.ucFirst(" ")); + assertEquals("A", StringUtils.ucFirst("A")); + assertEquals("1", StringUtils.ucFirst("1")); + assertEquals("A", StringUtils.ucFirst("a")); + assertEquals("Ab", StringUtils.ucFirst("ab")); + assertEquals("Abc", StringUtils.ucFirst("abc")); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 2a7db9bb..347d1417 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -47,6 +47,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput; @@ -140,6 +141,7 @@ public class TestUtils public static final String TABLE_NAME_ID_AND_NAME_ONLY = "idAndNameOnly"; public static final String TABLE_NAME_BASEPULL = "basepullTest"; public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport"; + public static final String REPORT_NAME_PERSON_JOIN_SHAPE = "simplePersonReport"; public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type @@ -192,6 +194,7 @@ public class TestUtils qInstance.addReport(defineShapesPersonsReport()); qInstance.addProcess(defineShapesPersonReportProcess()); + qInstance.addReport(definePersonJoinShapeReport()); qInstance.addAutomationProvider(definePollingAutomationProvider()); @@ -1108,4 +1111,26 @@ public class TestUtils .getProcessMetaData(); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QReportMetaData definePersonJoinShapeReport() + { + return new QReportMetaData() + .withName(REPORT_NAME_PERSON_JOIN_SHAPE) + .withDataSource( + new QReportDataSource() + .withSourceTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + ) + .withView(new QReportView() + .withType(ReportType.TABLE) + .withColumns(List.of( + new QReportField("id"), + new QReportField("firstName"), + new QReportField("lastName") + )) + ); + } } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 92acbc89..f4783a6a 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -926,8 +926,7 @@ public class QJavalinImplementation } catch(Exception e) { - pipedOutputStream.write(("Error generating report: " + e.getMessage()).getBytes()); - pipedOutputStream.close(); + handleExportOrReportException(context, pipedOutputStream, e); return (false); } }); @@ -953,6 +952,41 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void handleExportOrReportException(Context context, PipedOutputStream pipedOutputStream, Exception e) throws IOException + { + HttpStatus.Code statusCode = HttpStatus.Code.INTERNAL_SERVER_ERROR; // 500 + String message = e.getMessage(); + + QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class); + if(userFacingException != null) + { + LOG.info("User-facing exception", e); + statusCode = HttpStatus.Code.BAD_REQUEST; // 400 + message = userFacingException.getMessage(); + } + else + { + QAuthenticationException authenticationException = ExceptionUtils.findClassInRootChain(e, QAuthenticationException.class); + if(authenticationException != null) + { + statusCode = HttpStatus.Code.UNAUTHORIZED; // 401 + } + else + { + LOG.warn("Unexpected exception in javalin report or export request", e); + } + } + + context.status(statusCode.getCode()); + pipedOutputStream.write(("Error generating report: " + message).getBytes()); + pipedOutputStream.close(); + } + + + /******************************************************************************* ** *******************************************************************************/