diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java
index 7daf8de9..1d242057 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/processes/RunBackendStepAction.java
@@ -215,7 +215,7 @@ public class RunBackendStepAction
Object codeObject = codeClass.getConstructor().newInstance();
if(!(codeObject instanceof BackendStep backendStepCodeObject))
{
- throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of FunctionBody"));
+ throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of BackendStep"));
}
backendStepCodeObject.run(runBackendStepInput, runBackendStepOutput);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java
index eec134f0..83ec1dd1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java
@@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
+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;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
@@ -45,14 +46,24 @@ public class QueryAction
ActionHelper.validateSession(queryInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
- QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
+ QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
// todo pre-customization - just get to modify the request?
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);
// todo post-customization - can do whatever w/ the result if you want
- if (queryInput.getRecordPipe() == null)
+ if(queryInput.getRecordPipe() == null)
{
- QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords());
+ if(queryInput.getShouldGenerateDisplayValues())
+ {
+ QValueFormatter qValueFormatter = new QValueFormatter();
+ qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), queryOutput.getRecords());
+ }
+
+ if(queryInput.getShouldTranslatePossibleValues())
+ {
+ QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
+ qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), queryOutput.getRecords());
+ }
}
return queryOutput;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java
new file mode 100644
index 00000000..5f2cdc17
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QCustomPossibleValueProvider.java
@@ -0,0 +1,43 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.values;
+
+
+import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
+
+
+/*******************************************************************************
+ ** Interface to be implemented by user-defined code that serves as the backing
+ ** for a CUSTOM type possibleValueSource
+ *******************************************************************************/
+public interface QCustomPossibleValueProvider
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ QPossibleValue> getPossibleValue(Serializable idValue);
+
+ // todo - get/search list of possible values
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java
new file mode 100644
index 00000000..603951ee
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/values/QPossibleValueTranslator.java
@@ -0,0 +1,359 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.actions.values;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+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.QueryOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
+import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
+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 org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ ** Class responsible for looking up possible-values for fields/records and
+ ** make them into display values.
+ *******************************************************************************/
+public class QPossibleValueTranslator
+{
+ private static final Logger LOG = LogManager.getLogger(QPossibleValueTranslator.class);
+
+ private final QInstance qInstance;
+ private final QSession session;
+
+ // top-level keys are pvsNames (not table names)
+ // 2nd-level keys are pkey values from the PVS table
+ private Map> possibleValueCache;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QPossibleValueTranslator(QInstance qInstance, QSession session)
+ {
+ this.qInstance = qInstance;
+ this.session = session;
+
+ this.possibleValueCache = new HashMap<>();
+ }
+
+
+
+ /*******************************************************************************
+ ** For a list of records, translate their possible values (populating their display values)
+ *******************************************************************************/
+ public void translatePossibleValuesInRecords(QTableMetaData table, List records)
+ {
+ if(records == null)
+ {
+ return;
+ }
+
+ primePvsCache(table, records);
+
+ for(QRecord record : records)
+ {
+ for(QFieldMetaData field : table.getFields().values())
+ {
+ if(field.getPossibleValueSourceName() != null)
+ {
+ record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName())));
+ }
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ String translatePossibleValue(QFieldMetaData field, Serializable value)
+ {
+ QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
+ if(possibleValueSource == null)
+ {
+ LOG.error("Missing possible value source named [" + field.getPossibleValueSourceName() + "] when formatting value for field [" + field.getName() + "]");
+ return (null);
+ }
+
+ // todo - memoize!!!
+ // todo - bulk!!!
+
+ String resultValue = null;
+ if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM))
+ {
+ resultValue = translatePossibleValueEnum(value, possibleValueSource);
+ }
+ else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
+ {
+ resultValue = translatePossibleValueTable(field, value, possibleValueSource);
+ }
+ else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM))
+ {
+ resultValue = translatePossibleValueCustom(field, value, possibleValueSource);
+ }
+ else
+ {
+ LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "] on field [" + field.getName() + "]");
+ }
+
+ if(resultValue == null)
+ {
+ resultValue = getDefaultForPossibleValue(possibleValueSource, value);
+ }
+
+ return (resultValue);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
+ {
+ try
+ {
+ Class> codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName());
+ Object codeObject = codeClass.getConstructor().newInstance();
+ if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider))
+ {
+ throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider"));
+ }
+
+ return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value)));
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e);
+ }
+
+ return (null);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String translatePossibleValueTable(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
+ {
+ /////////////////////////////////
+ // null input gets null output //
+ /////////////////////////////////
+ if(value == null)
+ {
+ return (null);
+ }
+
+ //////////////////////////////////////////////////////////////
+ // look for cached value - if it's missing, call the primer //
+ //////////////////////////////////////////////////////////////
+ possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
+ Map cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
+ if(!cacheForPvs.containsKey(value))
+ {
+ primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), List.of(value));
+ }
+
+ return (cacheForPvs.get(value));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String formatPossibleValue(QPossibleValueSource possibleValueSource, QPossibleValue> possibleValue)
+ {
+ return (doFormatPossibleValue(possibleValueSource.getValueFormat(), possibleValueSource.getValueFields(), possibleValue.getId(), possibleValue.getLabel()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String getDefaultForPossibleValue(QPossibleValueSource possibleValueSource, Serializable value)
+ {
+ if(possibleValueSource.getValueFormatIfNotFound() == null)
+ {
+ return (null);
+ }
+
+ return (doFormatPossibleValue(possibleValueSource.getValueFormatIfNotFound(), possibleValueSource.getValueFieldsIfNotFound(), value, null));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @SuppressWarnings("checkstyle:Indentation")
+ private String doFormatPossibleValue(String formatString, List valueFields, Object id, String label)
+ {
+ List