mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-17 20:50:44 +00:00
Checkpoint on report and export changes, possible value translating
This commit is contained in:
@ -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??
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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<QFieldMetaData> 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<QRecord> 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<QRecord> 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<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)
|
||||
{
|
||||
QTableMetaData table = exportInput.getTable();
|
||||
List<QFieldMetaData> 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<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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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<QFieldMetaData> fields;
|
||||
if(CollectionUtils.nullSafeHasContents(reportView.getColumns()))
|
||||
List<QFieldMetaData> 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);
|
||||
}
|
||||
|
||||
|
@ -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<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.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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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<AbstractPostQueryCustomizer> 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())
|
||||
|
@ -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<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)
|
||||
{
|
||||
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<Serializable, String> cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
|
||||
Map<Serializable, String> 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<QRecord> records)
|
||||
void primePvsCache(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins)
|
||||
{
|
||||
ListingHash<String, QFieldMetaData> fieldsByPvsTable = new ListingHash<>();
|
||||
ListingHash<String, QPossibleValueSource> 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<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<>();
|
||||
|
||||
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<Serializable> values = new HashSet<>();
|
||||
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 //
|
||||
//////////////////////////////////////
|
||||
QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0);
|
||||
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
|
||||
Map<Serializable, String> cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
|
||||
Map<Serializable, String> 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<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
|
||||
** 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);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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<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...
|
||||
// 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()))
|
||||
{
|
||||
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<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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -42,6 +42,7 @@ public class QReportDataSource
|
||||
private List<QueryJoin> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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<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");
|
||||
|
||||
}
|
||||
|
||||
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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")
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
Reference in New Issue
Block a user