Merge branch 'feature/sprint-9-support-updates' into feature/QQQ-37-streamed-processes

This commit is contained in:
2022-08-23 11:17:45 -05:00
40 changed files with 1269 additions and 407 deletions

View File

@ -1,32 +0,0 @@
/*
* 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.customizers;
/*******************************************************************************
** Standard place where the names of QQQ Customization points are defined.
*******************************************************************************/
public interface Customizers
{
String POST_QUERY_RECORD = "postQueryRecord";
}

View File

@ -25,8 +25,11 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -44,15 +47,14 @@ public class QCodeLoader
/*******************************************************************************
**
*******************************************************************************/
public static Function<?, ?> getTableCustomizerFunction(QTableMetaData table, String customizerName)
public static <T, R> Optional<Function<T, R>> getTableCustomizerFunction(QTableMetaData table, String customizerName)
{
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
if(codeReference.isPresent())
{
return (QCodeLoader.getFunction(codeReference.get()));
return (Optional.ofNullable(QCodeLoader.getFunction(codeReference.get())));
}
return null;
return (Optional.empty());
}
@ -134,4 +136,29 @@ public class QCodeLoader
/*******************************************************************************
**
*******************************************************************************/
public static QCustomPossibleValueProvider getCustomPossibleValueProvider(QPossibleValueSource possibleValueSource) throws QException
{
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 (customPossibleValueProvider);
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e));
}
}
}

View File

@ -0,0 +1,61 @@
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.function.Consumer;
/*******************************************************************************
** Object used by TableCustomizers enum (and similar enums in backend modules)
** to assist with definition and validation of Customizers applied to tables.
*******************************************************************************/
public class TableCustomizer
{
private final String role;
private final Class<?> expectedType;
private final Consumer<Object> validationFunction;
/*******************************************************************************
**
*******************************************************************************/
public TableCustomizer(String role, Class<?> expectedType, Consumer<Object> validationFunction)
{
this.role = role;
this.expectedType = expectedType;
this.validationFunction = validationFunction;
}
/*******************************************************************************
** Getter for role
**
*******************************************************************************/
public String getRole()
{
return role;
}
/*******************************************************************************
** Getter for expectedType
**
*******************************************************************************/
public Class<?> getExpectedType()
{
return expectedType;
}
/*******************************************************************************
** Getter for validationFunction
**
*******************************************************************************/
public Consumer<Object> getValidationFunction()
{
return validationFunction;
}
}

View File

@ -0,0 +1,85 @@
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Enum definition of possible table customizers - "roles" for custom code that
** can be applied to tables.
**
** Works with TableCustomizer (singular version of this name) objects, during
** instance validation, to provide validation of the referenced code (and to
** make such validation from sub-backend-modules possible in the future).
**
** The idea of the 3rd argument here is to provide a way that we can enforce
** the type-parameters for the custom code. E.g., if it's a Function - how
** can we check at run-time that the type-params are correct? We couldn't find
** how to do this "reflectively", so we can instead try to run the custom code,
** passing it objects of the type that this customizer expects, and a validation
** error will raise upon ClassCastException... This maybe could improve!
*******************************************************************************/
public enum TableCustomizers
{
POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", Function.class, ((Object x) ->
{
Function<QRecord, QRecord> function = (Function<QRecord, QRecord>) x;
QRecord output = function.apply(new QRecord());
})));
private final TableCustomizer tableCustomizer;
/*******************************************************************************
**
*******************************************************************************/
TableCustomizers(TableCustomizer tableCustomizer)
{
this.tableCustomizer = tableCustomizer;
}
/*******************************************************************************
** Get the TableCustomer for a given role (e.g., the role used in meta-data, not
** the enum-constant name).
*******************************************************************************/
public static TableCustomizers forRole(String name)
{
for(TableCustomizers value : values())
{
if(value.tableCustomizer.getRole().equals(name))
{
return (value);
}
}
return (null);
}
/*******************************************************************************
** Getter for tableCustomizer
**
*******************************************************************************/
public TableCustomizer getTableCustomizer()
{
return tableCustomizer;
}
/*******************************************************************************
** get the role from the tableCustomizer
**
*******************************************************************************/
public String getRole()
{
return (tableCustomizer.getRole());
}
}

View File

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import org.apache.logging.log4j.LogManager;
@ -44,6 +45,13 @@ public class RecordPipe
private boolean isTerminated = false;
private Consumer<List<QRecord>> postRecordActions = null;
/////////////////////////////////////
// See usage below for explanation //
/////////////////////////////////////
private List<QRecord> singleRecordListForPostRecordActions = new ArrayList<>();
/*******************************************************************************
** Turn off the pipe. Stop accepting new records (just ignore them in the add
@ -69,6 +77,30 @@ public class RecordPipe
return;
}
if(postRecordActions != null)
{
////////////////////////////////////////////////////////////////////////////////////
// the initial use-case of this method is to call QueryAction.postRecordActions //
// that method requires that the list param be modifiable. Originally we used //
// List.of here - but that is immutable, so, instead use this single-record-list //
// (which we'll create as a field in this class, to avoid always re-constructing) //
////////////////////////////////////////////////////////////////////////////////////
singleRecordListForPostRecordActions.add(record);
postRecordActions.accept(singleRecordListForPostRecordActions);
record = singleRecordListForPostRecordActions.remove(0);
}
doAddRecord(record);
}
/*******************************************************************************
** Private internal version of add record - assumes the postRecordActions have
** already ran.
*******************************************************************************/
private void doAddRecord(QRecord record)
{
boolean offerResult = queue.offer(record);
while(!offerResult && !isTerminated)
@ -86,7 +118,15 @@ public class RecordPipe
*******************************************************************************/
public void addRecords(List<QRecord> records)
{
records.forEach(this::addRecord);
if(postRecordActions != null)
{
postRecordActions.accept(records);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// make sure to go to the private version of doAddRecord - to avoid re-running the post-actions //
//////////////////////////////////////////////////////////////////////////////////////////////////
records.forEach(this::doAddRecord);
}
@ -126,4 +166,14 @@ public class RecordPipe
return (queue.size());
}
/*******************************************************************************
**
*******************************************************************************/
public void setPostRecordActions(Consumer<List<QRecord>> postRecordActions)
{
this.postRecordActions = postRecordActions;
}
}

View File

@ -22,12 +22,18 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
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.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;
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;
@ -38,6 +44,14 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class QueryAction
{
private Optional<Function<QRecord, QRecord>> postQueryRecordCustomizer;
private QueryInput queryInput;
private QValueFormatter qValueFormatter;
private QPossibleValueTranslator qPossibleValueTranslator;
/*******************************************************************************
**
*******************************************************************************/
@ -45,6 +59,14 @@ public class QueryAction
{
ActionHelper.validateSession(queryInput);
postQueryRecordCustomizer = QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput;
if(queryInput.getRecordPipe() != null)
{
queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions);
}
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
// todo pre-customization - just get to modify the request?
@ -53,20 +75,42 @@ public class QueryAction
if(queryInput.getRecordPipe() == null)
{
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());
}
postRecordActions(queryOutput.getRecords());
}
return queryOutput;
}
/*******************************************************************************
** Run the necessary actions on a list of records (which must be a mutable list - e.g.,
** not one created via List.of()). This may include setting display values,
** translating possible values, and running post-record customizations.
*******************************************************************************/
public void postRecordActions(List<QRecord> records)
{
if(this.postQueryRecordCustomizer.isPresent())
{
records.replaceAll(t -> postQueryRecordCustomizer.get().apply(t));
}
if(queryInput.getShouldGenerateDisplayValues())
{
if(qValueFormatter == null)
{
qValueFormatter = new QValueFormatter();
}
qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
}
if(queryInput.getShouldTranslatePossibleValues())
{
if(qPossibleValueTranslator == null)
{
qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
}
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records);
}
}
}

View File

@ -31,8 +31,8 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
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;
@ -63,8 +63,10 @@ public class QPossibleValueTranslator
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
///////////////////////////////////////////////////////
// top-level keys are pvsNames (not table names) //
// 2nd-level keys are pkey values from the PVS table //
///////////////////////////////////////////////////////
private Map<String, Map<Serializable, String>> possibleValueCache;
@ -120,9 +122,6 @@ public class QPossibleValueTranslator
return (null);
}
// todo - memoize!!!
// todo - bulk!!!
String resultValue = null;
if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM))
{
@ -154,22 +153,14 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
*******************************************************************************/
private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource)
{
try
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
{
Class<?> codeClass = Class.forName(possibleValueSource.getCustomCodeReference().getName());
Object codeObject = codeClass.getConstructor().newInstance();
if(!(codeObject instanceof QCustomPossibleValueProvider customPossibleValueProvider))
if(possibleValue.getId().equals(value))
{
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of QCustomPossibleValueProvider"));
return (formatPossibleValue(possibleValueSource, possibleValue));
}
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);
@ -205,6 +196,26 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
*******************************************************************************/
private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
{
try
{
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
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);
}
/*******************************************************************************
**
*******************************************************************************/
@ -256,26 +267,11 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
*******************************************************************************/
private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource)
{
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
{
if(possibleValue.getId().equals(value))
{
return (formatPossibleValue(possibleValueSource, possibleValue));
}
}
return (null);
}
/*******************************************************************************
** prime the cache (e.g., by doing bulk-queries) for table-based PVS's
**
** @param table the table that the records are from
** @param records the records that have the possible value id's (e.g., foreign keys)
*******************************************************************************/
void primePvsCache(QTableMetaData table, List<QRecord> records)
{

View File

@ -25,10 +25,8 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable;
import java.util.List;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
@ -37,7 +35,7 @@ import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Utility to apply display formats to values for records and fields.
** Note that this includes handling PossibleValues.
**
*******************************************************************************/
public class QValueFormatter
{
@ -45,15 +43,6 @@ public class QValueFormatter
/*******************************************************************************
**
*******************************************************************************/
public QValueFormatter()
{
}
/*******************************************************************************
**
*******************************************************************************/
@ -67,16 +56,6 @@ public class QValueFormatter
return (null);
}
// todo - is this appropriate, with this class and possibleValueTransaltor being decoupled - to still do standard formatting here?
// alternatively, shold we return null here?
// ///////////////////////////////////////////////
// // if the field has a possible value, use it //
// ///////////////////////////////////////////////
// if(field.getPossibleValueSourceName() != null)
// {
// return (this.possibleValueTranslator.translatePossibleValue(field, value));
// }
////////////////////////////////////////////////////////
// if the field has a display format, try to apply it //
////////////////////////////////////////////////////////
@ -198,5 +177,4 @@ public class QValueFormatter
}
}
}

View File

@ -229,7 +229,21 @@ public class QInstanceEnricher
return (name.substring(0, 1).toUpperCase(Locale.ROOT));
}
return (name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1).replaceAll("([A-Z0-9]+)", " $1").replaceAll("([0-9])([A-Za-z])", "$1 $2"));
String suffix = name.substring(1)
//////////////////////////////////////////////////////////////////////
// Put a space before capital letters or numbers embedded in a name //
// e.g., omethingElse -> omething Else; umber1 -> umber 1 //
//////////////////////////////////////////////////////////////////////
.replaceAll("([A-Z0-9]+)", " $1")
////////////////////////////////////////////////////////////////
// put a space between numbers and words that come after them //
// e.g., umber1dad -> number 1 dad //
////////////////////////////////////////////////////////////////
.replaceAll("([0-9])([A-Za-z])", "$1 $2");
return (name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix);
}

View File

@ -22,13 +22,20 @@
package com.kingsrook.qqq.backend.core.instances;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
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.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
@ -38,6 +45,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -52,6 +61,11 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
*******************************************************************************/
public class QInstanceValidator
{
private static final Logger LOG = LogManager.getLogger(QInstanceValidator.class);
private boolean printWarnings = false;
/*******************************************************************************
**
@ -202,12 +216,130 @@ public class QInstanceValidator
assertCondition(errors, table.getFields().containsKey(recordLabelField), "Table " + tableName + " record label field " + recordLabelField + " is not a field on this table.");
}
}
if(table.getCustomizers() != null)
{
for(Map.Entry<String, QCodeReference> entry : table.getCustomizers().entrySet())
{
validateTableCustomizer(errors, tableName, entry.getKey(), entry.getValue());
}
}
});
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateTableCustomizer(List<String> errors, String tableName, String customizerName, QCodeReference codeReference)
{
String prefix = "Table " + tableName + ", customizer " + customizerName + ": ";
if(!preAssertionsForCodeReference(errors, codeReference, prefix))
{
return;
}
//////////////////////////////////////////////////////////////////////////////
// make sure (at this time) that it's a java type, then do some java checks //
//////////////////////////////////////////////////////////////////////////////
if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time."))
{
///////////////////////////////////////
// make sure the class can be loaded //
///////////////////////////////////////
Class<?> customizerClass = getClassForCodeReference(errors, codeReference, prefix);
if(customizerClass != null)
{
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass);
TableCustomizers tableCustomizer = TableCustomizers.forRole(customizerName);
if(tableCustomizer == null)
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - in the future, load customizers from backend-modules (e.g., FilesystemTableCustomizers) //
////////////////////////////////////////////////////////////////////////////////////////////////////
warn(prefix + "Unrecognized table customizer name (at least at backend-core level)");
}
else
{
////////////////////////////////////////////////////////////////////////
// make sure the customizer instance can be cast to the expected type //
////////////////////////////////////////////////////////////////////////
if(customizerInstance != null && tableCustomizer.getTableCustomizer().getExpectedType() != null)
{
Object castedObject = getCastedObject(errors, prefix, tableCustomizer.getTableCustomizer().getExpectedType(), customizerInstance);
Consumer<Object> validationFunction = tableCustomizer.getTableCustomizer().getValidationFunction();
if(castedObject != null && validationFunction != null)
{
try
{
validationFunction.accept(castedObject);
}
catch(ClassCastException e)
{
errors.add(prefix + "Error validating customizer type parameters: " + e.getMessage());
}
catch(Exception e)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// mmm, calling customizers w/ random data is expected to often throw, so, this check is iffy at best... //
// if we run into more trouble here, we might consider disabling the whole "validation function" check. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
}
}
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private <T> T getCastedObject(List<String> errors, String prefix, Class<T> expectedType, Object customizerInstance)
{
T castedObject = null;
try
{
castedObject = expectedType.cast(customizerInstance);
}
catch(ClassCastException e)
{
errors.add(prefix + "CodeReference could not be casted to the expected type: " + expectedType);
}
return castedObject;
}
/*******************************************************************************
**
*******************************************************************************/
private Object getInstanceOfCodeReference(List<String> errors, String prefix, Class<?> customizerClass)
{
Object customizerInstance = null;
try
{
customizerInstance = customizerClass.getConstructor().newInstance();
}
catch(InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e)
{
errors.add(prefix + "Instance of CodeReference could not be created: " + e);
}
return customizerInstance;
}
/*******************************************************************************
**
*******************************************************************************/
@ -313,7 +445,6 @@ public class QInstanceValidator
qInstance.getPossibleValueSources().forEach((pvsName, possibleValueSource) ->
{
assertCondition(errors, Objects.equals(pvsName, possibleValueSource.getName()), "Inconsistent naming for possibleValueSource: " + pvsName + "/" + possibleValueSource.getName() + ".");
assertCondition(errors, possibleValueSource.getIdType() != null, "Missing an idType for possibleValueSource: " + pvsName);
if(assertCondition(errors, possibleValueSource.getType() != null, "Missing type for possibleValueSource: " + pvsName))
{
////////////////////////////////////////////////////////////////////////////////////////////////
@ -347,6 +478,7 @@ public class QInstanceValidator
if(assertCondition(errors, possibleValueSource.getCustomCodeReference() != null, "custom-type possibleValueSource " + pvsName + " is missing a customCodeReference."))
{
assertCondition(errors, QCodeUsage.POSSIBLE_VALUE_PROVIDER.equals(possibleValueSource.getCustomCodeReference().getCodeUsage()), "customCodeReference for possibleValueSource " + pvsName + " is not a possibleValueProvider.");
validateCustomPossibleValueSourceCode(errors, pvsName, possibleValueSource.getCustomCodeReference());
}
}
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
@ -358,6 +490,87 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateCustomPossibleValueSourceCode(List<String> errors, String pvsName, QCodeReference codeReference)
{
String prefix = "PossibleValueSource " + pvsName + " custom code reference: ";
if(!preAssertionsForCodeReference(errors, codeReference, prefix))
{
return;
}
//////////////////////////////////////////////////////////////////////////////
// make sure (at this time) that it's a java type, then do some java checks //
//////////////////////////////////////////////////////////////////////////////
if(assertCondition(errors, codeReference.getCodeType().equals(QCodeType.JAVA), prefix + "Only JAVA customizers are supported at this time."))
{
///////////////////////////////////////
// make sure the class can be loaded //
///////////////////////////////////////
Class<?> customizerClass = getClassForCodeReference(errors, codeReference, prefix);
if(customizerClass != null)
{
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
Object customizerInstance = getInstanceOfCodeReference(errors, prefix, customizerClass);
////////////////////////////////////////////////////////////////////////
// make sure the customizer instance can be cast to the expected type //
////////////////////////////////////////////////////////////////////////
if(customizerInstance != null)
{
getCastedObject(errors, prefix, QCustomPossibleValueProvider.class, customizerInstance);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private Class<?> getClassForCodeReference(List<String> errors, QCodeReference codeReference, String prefix)
{
Class<?> customizerClass = null;
try
{
customizerClass = Class.forName(codeReference.getName());
}
catch(ClassNotFoundException e)
{
errors.add(prefix + "Class for CodeReference could not be found.");
}
return customizerClass;
}
/*******************************************************************************
**
*******************************************************************************/
private boolean preAssertionsForCodeReference(List<String> errors, QCodeReference codeReference, String prefix)
{
boolean okay = true;
if(!assertCondition(errors, StringUtils.hasContent(codeReference.getName()), prefix + " is missing a code reference name"))
{
okay = false;
}
if(!assertCondition(errors, codeReference.getCodeType() != null, prefix + " is missing a code type"))
{
okay = false;
}
return (okay);
}
/*******************************************************************************
** Check if an app's child list can recursively be traversed without finding a
** duplicate, which would indicate a cycle (e.g., an error)
@ -410,4 +623,16 @@ public class QInstanceValidator
return (condition);
}
/*******************************************************************************
**
*******************************************************************************/
private void warn(String message)
{
if(printWarnings)
{
LOG.info("Validation warning: " + message);
}
}
}

View File

@ -24,13 +24,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.Customizers;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -39,12 +34,8 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QueryOutput extends AbstractActionOutput implements Serializable
{
private static final Logger LOG = LogManager.getLogger(QueryOutput.class);
private QueryOutputStorageInterface storage;
private Function<QRecord, QRecord> postQueryRecordCustomizer;
/*******************************************************************************
@ -61,8 +52,6 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
{
storage = new QueryOutputList();
}
postQueryRecordCustomizer = (Function<QRecord, QRecord>) QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD);
}
@ -76,36 +65,16 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
*******************************************************************************/
public void addRecord(QRecord record)
{
record = runPostQueryRecordCustomizer(record);
storage.addRecord(record);
}
/*******************************************************************************
**
*******************************************************************************/
public QRecord runPostQueryRecordCustomizer(QRecord record)
{
if(this.postQueryRecordCustomizer != null)
{
record = this.postQueryRecordCustomizer.apply(record);
}
return record;
}
/*******************************************************************************
** add a list of records to this output
*******************************************************************************/
public void addRecords(List<QRecord> records)
{
if(this.postQueryRecordCustomizer != null)
{
records.replaceAll(t -> this.postQueryRecordCustomizer.apply(t));
}
storage.addRecords(records);
}

View File

@ -334,7 +334,7 @@ public class QRecord implements Serializable
*******************************************************************************/
public LocalTime getValueLocalTime(String fieldName)
{
return ((LocalTime) ValueUtils.getValueAsLocalTime(values.get(fieldName)));
return (ValueUtils.getValueAsLocalTime(values.get(fieldName)));
}

View File

@ -0,0 +1,55 @@
package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import java.util.List;
/*******************************************************************************
** Define some standard ways to format the value portion of a PossibleValueSource.
**
** Can be passed to short-cut {set,with}ValueFormatAndFields methods in QPossibleValueSource
** class, or the format & field properties can be extracted and passed to regular field-level setters.
*******************************************************************************/
public enum PVSValueFormatAndFields
{
LABEL_ONLY("%s", "label"),
LABEL_PARENS_ID("%s (%s)", "label", "id"),
ID_COLON_LABEL("%s: %s", "id", "label");
private final String format;
private final List<String> fields;
/*******************************************************************************
**
*******************************************************************************/
PVSValueFormatAndFields(String format, String... fields)
{
this.format = format;
this.fields = List.of(fields);
}
/*******************************************************************************
** Getter for format
**
*******************************************************************************/
public String getFormat()
{
return format;
}
/*******************************************************************************
** Getter for fields
**
*******************************************************************************/
public List<String> getFields()
{
return fields;
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
/*******************************************************************************
** Interface to be implemented by enums which can be used as a PossibleValueSource.
**
*******************************************************************************/
public interface PossibleValueEnum<T>

View File

@ -25,44 +25,24 @@ package com.kingsrook.qqq.backend.core.model.metadata.possiblevalues;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
/*******************************************************************************
** Meta-data to represent a single field in a table.
** Meta-data to represent a "Possible value" - e.g., a translation of a foreign
** key and/or a limited set of "possible values" for a field (e.g., from a foreign
** table or an enum).
**
*******************************************************************************/
public class QPossibleValueSource
{
private String name;
private QPossibleValueSourceType type;
private QFieldType idType = QFieldType.INTEGER;
private String valueFormat = ValueFormat.DEFAULT;
private List<String> valueFields = ValueFields.DEFAULT;
private String valueFormat = PVSValueFormatAndFields.LABEL_ONLY.getFormat();
private List<String> valueFields = PVSValueFormatAndFields.LABEL_ONLY.getFields();
private String valueFormatIfNotFound = null;
private List<String> valueFieldsIfNotFound = null;
public interface ValueFormat
{
String DEFAULT = "%s";
String LABEL_ONLY = "%s";
String LABEL_PARENS_ID = "%s (%s)";
String ID_COLON_LABEL = "%s: %s";
}
public interface ValueFields
{
List<String> DEFAULT = List.of("label");
List<String> LABEL_ONLY = List.of("label");
List<String> LABEL_PARENS_ID = List.of("label", "id");
List<String> ID_COLON_LABEL = List.of("id", "label");
}
// todo - optimization hints, such as "table is static, fully cache" or "table is small, so we can pull the whole thing into memory?"
//////////////////////
@ -154,40 +134,6 @@ public class QPossibleValueSource
/*******************************************************************************
** Getter for idType
**
*******************************************************************************/
public QFieldType getIdType()
{
return idType;
}
/*******************************************************************************
** Setter for idType
**
*******************************************************************************/
public void setIdType(QFieldType idType)
{
this.idType = idType;
}
/*******************************************************************************
** Fluent setter for idType
**
*******************************************************************************/
public QPossibleValueSource withIdType(QFieldType idType)
{
this.idType = idType;
return (this);
}
/*******************************************************************************
** Getter for valueFormat
**
@ -407,6 +353,9 @@ public class QPossibleValueSource
/*******************************************************************************
** This is the easiest way to add the values from an enum to a PossibleValueSource.
** Make sure the enum implements PossibleValueEnum - then call as:
** myPossibleValueSource.withValuesFromEnum(MyEnum.values()));
**
*******************************************************************************/
public <T extends PossibleValueEnum<?>> QPossibleValueSource withValuesFromEnum(T[] values)
@ -453,4 +402,26 @@ public class QPossibleValueSource
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void setValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields)
{
this.valueFormat = valueFormatAndFields.getFormat();
this.valueFields = valueFormatAndFields.getFields();
}
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueSource withValueFormatAndFields(PVSValueFormatAndFields valueFormatAndFields)
{
setValueFormatAndFields(valueFormatAndFields);
return (this);
}
}

View File

@ -30,6 +30,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField;
@ -85,6 +86,17 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return ("QTableMetaData[" + name + "]");
}
/*******************************************************************************
**
*******************************************************************************/
@ -451,6 +463,16 @@ public class QTableMetaData implements QAppChildMetaData, Serializable
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData withCustomizer(TableCustomizer tableCustomizer, QCodeReference customizer)
{
return (withCustomizer(tableCustomizer.getRole(), customizer));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -174,10 +174,13 @@ public class MemoryRecordStore
*******************************************************************************/
public Integer count(CountInput input)
{
Map<Serializable, QRecord> tableData = getTableData(input.getTable());
List<QRecord> records = new ArrayList<>(tableData.values());
// todo - filtering (call query)
return (records.size());
QueryInput queryInput = new QueryInput(input.getInstance());
queryInput.setSession(input.getSession());
queryInput.setTableName(input.getTableName());
queryInput.setFilter(input.getFilter());
List<QRecord> queryResult = query(queryInput);
return (queryResult.size());
}
@ -192,27 +195,43 @@ public class MemoryRecordStore
return (new ArrayList<>());
}
QTableMetaData table = input.getTable();
Map<Serializable, QRecord> tableData = getTableData(table);
Integer nextSerial = nextSerials.get(table.getName());
QTableMetaData table = input.getTable();
Map<Serializable, QRecord> tableData = getTableData(table);
////////////////////////////////////////
// grab the next unique serial to use //
////////////////////////////////////////
Integer nextSerial = nextSerials.get(table.getName());
if(nextSerial == null)
{
nextSerial = 1;
while(tableData.containsKey(nextSerial))
{
nextSerial++;
}
}
while(tableData.containsKey(nextSerial))
{
nextSerial++;
}
List<QRecord> outputRecords = new ArrayList<>();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
for(QRecord record : input.getRecords())
{
/////////////////////////////////////////////////
// set the next serial in the record if needed //
/////////////////////////////////////////////////
if(record.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER))
{
record.setValue(primaryKeyField.getName(), nextSerial++);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// make sure that if the user supplied a serial, greater than the one we had, that we skip ahead //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(primaryKeyField.getType().equals(QFieldType.INTEGER) && record.getValueInteger(primaryKeyField.getName()) > nextSerial)
{
nextSerial = record.getValueInteger(primaryKeyField.getName()) + 1;
}
tableData.put(record.getValue(primaryKeyField.getName()), record);
if(returnInsertedRecords)
{
@ -220,6 +239,8 @@ public class MemoryRecordStore
}
}
nextSerials.put(table.getName(), nextSerial);
return (outputRecords);
}
@ -256,10 +277,6 @@ public class MemoryRecordStore
outputRecords.add(record);
}
}
else
{
outputRecords.add(record);
}
}
return (outputRecords);

View File

@ -423,7 +423,7 @@ public class ValueUtils
/*******************************************************************************
**
*******************************************************************************/
public static Object getValueAsLocalTime(Serializable value)
public static LocalTime getValueAsLocalTime(Serializable value)
{
try
{