Checkpoint on report and export changes, possible value translating

This commit is contained in:
2022-12-21 11:37:16 -06:00
parent 19d88910b5
commit 799b695e14
18 changed files with 773 additions and 91 deletions

View File

@ -92,6 +92,12 @@ public class RecordAutomationStatusUpdater
{ {
for(QRecord record : records) 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()); record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
// todo - another field - for the automation timestamp?? // todo - another field - for the automation timestamp??
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QRecord> 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();
}
}
}

View File

@ -247,14 +247,6 @@ public class ExcelExportStreamer implements ExportStreamerInterface
for(QFieldMetaData field : fields) for(QFieldMetaData field : fields)
{ {
Serializable value = qRecord.getValue(field.getName()); Serializable value = qRecord.getValue(field.getName());
if(field.getPossibleValueSourceName() != null)
{
String displayValue = qRecord.getDisplayValue(field.getName());
if(displayValue != null)
{
value = displayValue;
}
}
if(value != null) if(value != null)
{ {

View File

@ -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.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; 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.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.QException;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException; import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; 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.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -147,17 +148,18 @@ public class ExportAction
////////////////////////// //////////////////////////
// set up a query input // // set up a query input //
////////////////////////// //////////////////////////
QueryInterface queryInterface = backendModule.getQueryInterface(); QueryAction queryAction = new QueryAction();
QueryInput queryInput = new QueryInput(exportInput.getInstance()); QueryInput queryInput = new QueryInput(exportInput.getInstance());
queryInput.setSession(exportInput.getSession()); queryInput.setSession(exportInput.getSession());
queryInput.setTableName(exportInput.getTableName()); queryInput.setTableName(exportInput.getTableName());
queryInput.setFilter(exportInput.getQueryFilter()); queryInput.setFilter(exportInput.getQueryFilter());
queryInput.setLimit(exportInput.getLimit()); queryInput.setLimit(exportInput.getLimit());
queryInput.setShouldTranslatePossibleValues(true);
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// tell this query that it needs to put its output into a pipe // // 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); queryInput.setRecordPipe(recordPipe);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -165,13 +167,14 @@ public class ExportAction
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ReportFormat reportFormat = exportInput.getReportFormat(); ReportFormat reportFormat = exportInput.getReportFormat();
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer(); ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
reportStreamer.start(exportInput, getFields(exportInput), "Sheet 1"); List<QFieldMetaData> fields = getFields(exportInput);
reportStreamer.start(exportInput, fields, "Sheet 1");
////////////////////////////////////////// //////////////////////////////////////////
// run the query action as an async job // // run the query action as an async job //
////////////////////////////////////////// //////////////////////////////////////////
AsyncJobManager asyncJobManager = new AsyncJobManager(); 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"); LOG.info("Started query job [" + queryJobUUID + "] for report");
AsyncJobState queryJobState = AsyncJobState.RUNNING; AsyncJobState queryJobState = AsyncJobState.RUNNING;
@ -209,7 +212,7 @@ public class ExportAction
nextSleepMillis = INIT_SLEEP_MS; nextSleepMillis = INIT_SLEEP_MS;
List<QRecord> records = recordPipe.consumeAvailableRecords(); List<QRecord> records = recordPipe.consumeAvailableRecords();
reportStreamer.addRecords(records); processRecords(reportStreamer, fields, records);
recordCount += records.size(); recordCount += records.size();
LOG.info(countFromPreExecute != null LOG.info(countFromPreExecute != null
@ -238,7 +241,7 @@ public class ExportAction
// send the final records to the report streamer // // send the final records to the report streamer //
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
List<QRecord> records = recordPipe.consumeAvailableRecords(); List<QRecord> records = recordPipe.consumeAvailableRecords();
reportStreamer.addRecords(records); processRecords(reportStreamer, fields, records);
recordCount += records.size(); recordCount += records.size();
long reportEndTime = System.currentTimeMillis(); long reportEndTime = System.currentTimeMillis();
@ -269,20 +272,59 @@ public class ExportAction
/*******************************************************************************
**
*******************************************************************************/
private static void processRecords(ExportStreamerInterface reportStreamer, List<QFieldMetaData> fields, List<QRecord> 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<QFieldMetaData> getFields(ExportInput exportInput) private List<QFieldMetaData> getFields(ExportInput exportInput)
{ {
List<QFieldMetaData> fieldList;
QTableMetaData table = exportInput.getTable(); QTableMetaData table = exportInput.getTable();
if(exportInput.getFieldNames() != null) if(exportInput.getFieldNames() != null)
{ {
return (exportInput.getFieldNames().stream().map(table::getField).toList()); fieldList = exportInput.getFieldNames().stream().map(table::getField).toList();
} }
else else
{ {
return (new ArrayList<>(table.getFields().values())); fieldList = new ArrayList<>(table.getFields().values());
} }
//////////////////////////////////////////
// add fields for possible value labels //
//////////////////////////////////////////
List<QFieldMetaData> 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);
} }

View File

@ -29,11 +29,13 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop; 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.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.reporting.customizers.ReportViewCustomizer;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; 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.AggregatesInterface;
import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates; import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates; 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 public class GenerateReportAction
{ {
private static final Logger LOG = LogManager.getLogger(GenerateReportAction.class);
///////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////
// summaryAggregates and varianceAggregates are multi-level maps, ala: // // summaryAggregates and varianceAggregates are multi-level maps, ala: //
// viewName > SummaryKey > fieldName > Aggregates // // viewName > SummaryKey > fieldName > Aggregates //
@ -214,10 +220,7 @@ public class GenerateReportAction
JoinsContext joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins()); JoinsContext joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins());
List<QFieldMetaData> fields; List<QFieldMetaData> fields = new ArrayList<>();
if(CollectionUtils.nullSafeHasContents(reportView.getColumns()))
{
fields = new ArrayList<>();
for(QReportField column : reportView.getColumns()) for(QReportField column : reportView.getColumns())
{ {
if(column.getIsVirtual()) if(column.getIsVirtual())
@ -226,10 +229,11 @@ public class GenerateReportAction
} }
else else
{ {
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(column.getName()); String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(effectiveFieldName);
if(fieldAndTableNameOrAlias.field() == null) if(fieldAndTableNameOrAlias.field() == null)
{ {
throw new QReportingException("Could not find field named [" + column.getName() + "] on table [" + table.getName() + "]"); throw new QReportingException("Could not find field named [" + effectiveFieldName + "] on table [" + table.getName() + "]");
} }
QFieldMetaData field = fieldAndTableNameOrAlias.field().clone(); QFieldMetaData field = fieldAndTableNameOrAlias.field().clone();
@ -241,11 +245,7 @@ public class GenerateReportAction
fields.add(field); fields.add(field);
} }
} }
}
else
{
fields = new ArrayList<>(table.getFields().values());
}
reportStreamer.setDisplayFormats(getDisplayFormatMap(fields)); reportStreamer.setDisplayFormats(getDisplayFormatMap(fields));
reportStreamer.start(exportInput, fields, reportView.getLabel()); 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 // // 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) -> new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
{ {
if(dataSource.getSourceTable() != null) if(dataSource.getSourceTable() != null)
@ -301,6 +301,13 @@ public class GenerateReportAction
queryInput.setFilter(queryFilter); queryInput.setFilter(queryFilter);
queryInput.setQueryJoins(dataSource.getQueryJoins()); queryInput.setQueryJoins(dataSource.getQueryJoins());
queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this? 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)); return (new QueryAction().execute(queryInput));
} }
else if(dataSource.getStaticDataSupplier() != null) else if(dataSource.getStaticDataSupplier() != null)
@ -369,7 +376,8 @@ public class GenerateReportAction
for(Serializable value : criterion.getValues()) for(Serializable value : criterion.getValues())
{ {
String valueAsString = ValueUtils.getValueAsString(value); String valueAsString = ValueUtils.getValueAsString(value);
Serializable interpretedValue = variableInterpreter.interpret(valueAsString); // Serializable interpretedValue = variableInterpreter.interpret(valueAsString);
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
newValues.add(interpretedValue); newValues.add(interpretedValue);
} }
criterion.setValues(newValues); criterion.setValues(newValues);
@ -391,6 +399,22 @@ public class GenerateReportAction
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
if(tableView != null) 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); reportStreamer.addRecords(records);
} }

View File

@ -24,14 +24,18 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException; import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput; import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -49,6 +53,7 @@ public class JsonExportStreamer implements ExportStreamerInterface
private OutputStream outputStream; private OutputStream outputStream;
private boolean needComma = false; private boolean needComma = false;
private boolean prettyPrint = true;
@ -74,7 +79,7 @@ public class JsonExportStreamer implements ExportStreamerInterface
try try
{ {
outputStream.write("[".getBytes(StandardCharsets.UTF_8)); outputStream.write('[');
} }
catch(IOException e) catch(IOException e)
{ {
@ -109,11 +114,25 @@ public class JsonExportStreamer implements ExportStreamerInterface
{ {
if(needComma) if(needComma)
{ {
outputStream.write(",".getBytes(StandardCharsets.UTF_8)); outputStream.write(',');
}
Map<String, Serializable> 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.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush(); // todo - less often? outputStream.flush(); // todo - less often?
needComma = true; needComma = true;
} }
@ -144,7 +163,11 @@ public class JsonExportStreamer implements ExportStreamerInterface
{ {
try try
{ {
outputStream.write("]".getBytes(StandardCharsets.UTF_8)); if(prettyPrint)
{
outputStream.write('\n');
}
outputStream.write(']');
} }
catch(IOException e) catch(IOException e)
{ {

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}

View File

@ -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.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; 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.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.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.model.data.QRecord;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; 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 public class QueryAction
{ {
private static final Logger LOG = LogManager.getLogger(QueryAction.class);
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer; private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
private QueryInput queryInput; private QueryInput queryInput;
@ -72,6 +77,11 @@ public class QueryAction
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput); QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);
// todo post-customization - can do whatever w/ the result if you want // todo post-customization - can do whatever w/ the result if you want
if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe)
{
bufferedRecordPipe.finalFlush();
}
if(queryInput.getRecordPipe() == null) if(queryInput.getRecordPipe() == null)
{ {
postRecordActions(queryOutput.getRecords()); postRecordActions(queryOutput.getRecords());
@ -100,7 +110,7 @@ public class QueryAction
{ {
qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession()); qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
} }
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records); qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records, queryInput.getQueryJoins());
} }
if(queryInput.getShouldGenerateDisplayValues()) if(queryInput.getShouldGenerateDisplayValues())

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; 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.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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.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.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.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.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash; 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 com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 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) ** For a list of records, translate their possible values (populating their display values)
*******************************************************************************/ *******************************************************************************/
public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> records) public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> 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<QRecord> records, List<QueryJoin> queryJoins)
{ {
if(records == null || table == null) if(records == null || table == null)
{ {
return; 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) for(QRecord record : records)
{ {
@ -108,6 +123,42 @@ public class QPossibleValueTranslator
record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName()))); 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 // // look for cached value - if it's missing, call the primer //
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); Map<Serializable, String> cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>());
Map<Serializable, String> cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
if(!cacheForPvs.containsKey(value)) if(!cacheForPvs.containsKey(value))
{ {
primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), List.of(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 ** 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 records the records that have the possible value id's (e.g., foreign keys)
* @param queryJoins
*******************************************************************************/ *******************************************************************************/
void primePvsCache(QTableMetaData table, List<QRecord> records) void primePvsCache(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins)
{ {
ListingHash<String, QFieldMetaData> fieldsByPvsTable = new ListingHash<>(); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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<String, Pair<String, QFieldMetaData>> fieldsByPvsTable = new ListingHash<>();
///////////////////////////////////////////////////////////////////////////////////////
// this is a map of String(tableName - the PVS table) to PossibleValueSource objects //
///////////////////////////////////////////////////////////////////////////////////////
ListingHash<String, QPossibleValueSource> pvsesByTable = new ListingHash<>(); ListingHash<String, QPossibleValueSource> pvsesByTable = new ListingHash<>();
for(QFieldMetaData field : table.getFields().values())
primePvsCacheTableListingHashLoader(table, fieldsByPvsTable, pvsesByTable, "");
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins))
{ {
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName()); if(queryJoin.getSelect())
if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{ {
fieldsByPvsTable.add(possibleValueSource.getTableName(), field); // todo - aliases probably not handled right
pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource); primePvsCacheTableListingHashLoader(qInstance.getTable(queryJoin.getRightTable()), fieldsByPvsTable, pvsesByTable, queryJoin.getRightTable() + ".");
} }
} }
@ -352,16 +410,24 @@ public class QPossibleValueTranslator
Set<Serializable> values = new HashSet<>(); Set<Serializable> values = new HashSet<>();
for(QRecord record : records) for(QRecord record : records)
{ {
for(QFieldMetaData field : fieldsByPvsTable.get(tableName)) for(Pair<String, QFieldMetaData> 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 // // check if value is already cached //
////////////////////////////////////// //////////////////////////////////////
QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0); QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0);
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>()); Map<Serializable, String> cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>());
Map<Serializable, String> cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
if(!cacheForPvs.containsKey(fieldValue)) if(!cacheForPvs.containsKey(fieldValue))
{ {
@ -379,6 +445,28 @@ public class QPossibleValueTranslator
/*******************************************************************************
** Helper for the primePvsCache method
*******************************************************************************/
private void primePvsCacheTableListingHashLoader(QTableMetaData table, ListingHash<String, Pair<String, QFieldMetaData>> fieldsByPvsTable, ListingHash<String, QPossibleValueSource> 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 ** 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, ** possible value sources based on that table (maybe usually 1, but could be more,
@ -408,10 +496,12 @@ public class QPossibleValueTranslator
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
if(notTooDeep()) if(notTooDeep())
{ {
queryInput.setShouldTranslatePossibleValues(true); // todo not commit...
// queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true); queryInput.setShouldGenerateDisplayValues(true);
} }
LOG.debug("Priming PVS cache for [" + page.size() + "] ids from [" + tableName + "] table.");
QueryOutput queryOutput = new QueryAction().execute(queryInput); QueryOutput queryOutput = new QueryAction().execute(queryInput);
/////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////

View File

@ -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.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; 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.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.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; 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.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; 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.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.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
@ -984,7 +986,7 @@ public class QInstanceValidator
{ {
if(dataSource.getQueryFilter() != null) 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 + ".")) if(assertCondition(CollectionUtils.nullSafeHasContents(report.getViews()), "At least 1 view must be defined in report " + reportName + "."))
{ {
int index = 0; int index = 0;
@ -1015,19 +1017,30 @@ public class QInstanceValidator
usedViewNames.add(view.getName()); usedViewNames.add(view.getName());
String viewErrorPrefix = "Report " + reportName + " view " + view.getName() + " "; String viewErrorPrefix = "Report " + reportName + " view " + view.getName() + " ";
assertCondition(view.getType() != null, viewErrorPrefix + " is missing its type."); assertCondition(view.getType() != null, viewErrorPrefix + "is missing its type.");
if(assertCondition(StringUtils.hasContent(view.getDataSourceName()), viewErrorPrefix + " is missing a dataSourceName")) 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())) 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... boolean hasColumns = CollectionUtils.nullSafeHasContents(view.getColumns());
assertCondition(CollectionUtils.nullSafeHasContents(view.getColumns()), viewErrorPrefix + " does not have any columns."); boolean hasViewCustomizer = view.getViewCustomizer() != null;
assertCondition(hasColumns || hasViewCustomizer, viewErrorPrefix + "does not have any columns or a view customizer.");
Set<String> 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... // todo - all these too...
// view.getPivotFields(); // 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<QueryJoin> queryJoins)
{ {
for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria())) 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.getOperator() != null, context + "Missing operator for a criteria on fieldName " + fieldName);
assertCondition(criterion.getValues() != null, context + "Missing values for a criteria on fieldName " + criterion.getFieldName()); // todo - what about ops w/ no value (BLANK) 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())) for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
{ {
if(assertCondition(StringUtils.hasContent(orderBy.getFieldName()), context + "Missing fieldName for an orderBy")) 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())) 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<QueryJoin> 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;
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -42,6 +42,7 @@ public class QReportDataSource
private List<QueryJoin> queryJoins = null; private List<QueryJoin> queryJoins = null;
private QCodeReference queryInputCustomizer;
private QCodeReference staticDataSupplier; private QCodeReference staticDataSupplier;
@ -230,4 +231,38 @@ public class QReportDataSource
return (this); 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);
}
} }

View File

@ -43,6 +43,9 @@ public class QReportField
private boolean isVirtual = false; private boolean isVirtual = false;
private boolean showPossibleValueLabel = false;
private String sourceFieldName;
/******************************************************************************* /*******************************************************************************
@ -281,4 +284,73 @@ public class QReportField
this.isVirtual = isVirtual; this.isVirtual = isVirtual;
return (this); 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);
}
} }

View File

@ -361,4 +361,44 @@ public class StringUtils
return (size != null && size.equals(1) ? ifOne : ifNotOne); 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));
}
} }

View File

@ -188,7 +188,7 @@ public class QPossibleValueTranslatorTest
); );
QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON); QTableMetaData personTable = qInstance.getTable(TestUtils.TABLE_NAME_PERSON);
MemoryRecordStore.resetStatistics(); 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"); assertEquals(1, MemoryRecordStore.getStatistics().get(MemoryRecordStore.STAT_QUERIES_RAN), "Should only run 1 query");
possibleValueTranslator.translatePossibleValue(shapeField, 1); possibleValueTranslator.translatePossibleValue(shapeField, 1);
possibleValueTranslator.translatePossibleValue(shapeField, 2); possibleValueTranslator.translatePossibleValue(shapeField, 2);

View File

@ -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.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData; 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.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.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -1436,6 +1437,69 @@ class QInstanceValidatorTest
dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class, null)); dataSource.setStaticDataSupplier(new QCodeReference(ArrayList.class, null));
}, },
"is not of the expected type"); "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<QReportField> 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");
} }

View File

@ -182,6 +182,7 @@ class StringUtilsTest
} }
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -246,4 +247,41 @@ class StringUtilsTest
assertEquals("", StringUtils.plural(1, "", "es")); assertEquals("", StringUtils.plural(1, "", "es"));
assertEquals("es", StringUtils.plural(2, "", "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"));
}
} }

View File

@ -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.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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.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.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput; 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_ID_AND_NAME_ONLY = "idAndNameOnly";
public static final String TABLE_NAME_BASEPULL = "basepullTest"; public static final String TABLE_NAME_BASEPULL = "basepullTest";
public static final String REPORT_NAME_SHAPES_PERSON = "shapesPersonReport"; 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_STATE = "state"; // enum-type
public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type
@ -192,6 +194,7 @@ public class TestUtils
qInstance.addReport(defineShapesPersonsReport()); qInstance.addReport(defineShapesPersonsReport());
qInstance.addProcess(defineShapesPersonReportProcess()); qInstance.addProcess(defineShapesPersonReportProcess());
qInstance.addReport(definePersonJoinShapeReport());
qInstance.addAutomationProvider(definePollingAutomationProvider()); qInstance.addAutomationProvider(definePollingAutomationProvider());
@ -1108,4 +1111,26 @@ public class TestUtils
.getProcessMetaData(); .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")
))
);
}
} }

View File

@ -926,8 +926,7 @@ public class QJavalinImplementation
} }
catch(Exception e) catch(Exception e)
{ {
pipedOutputStream.write(("Error generating report: " + e.getMessage()).getBytes()); handleExportOrReportException(context, pipedOutputStream, e);
pipedOutputStream.close();
return (false); 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();
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/