diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml
index 2b0530f7..17fdf949 100644
--- a/qqq-backend-core/pom.xml
+++ b/qqq-backend-core/pom.xml
@@ -129,10 +129,16 @@
2.16.0
+
+ com.nimbusds
+ oauth2-oidc-sdk
+ 11.23.1
+
+
com.auth0
auth0
- 2.1.0
+ 2.18.0
com.auth0
@@ -142,12 +148,12 @@
com.auth0
jwks-rsa
- 0.22.0
+ 0.22.1
io.github.cdimascio
- java-dotenv
- 5.2.2
+ dotenv-java
+ 3.2.0
org.apache.velocity
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java
index 9b7fd619..1b30f82e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java
@@ -29,7 +29,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
** Version of AbstractQQQApplication that assumes all meta-data is produced
- ** by MetaDataProducers in a single package.
+ ** by MetaDataProducers in (or under) a single package.
*******************************************************************************/
public abstract class AbstractMetaDataProducerBasedQQQApplication extends AbstractQQQApplication
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/ConfigFilesBasedQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/ConfigFilesBasedQQQApplication.java
new file mode 100644
index 00000000..633c34d4
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/ConfigFilesBasedQQQApplication.java
@@ -0,0 +1,61 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.instances;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.instances.loaders.MetaDataLoaderHelper;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+
+
+/*******************************************************************************
+ ** Version of AbstractQQQApplication that assumes all meta-data is defined in
+ ** config files (yaml, json, etc) under a given directory path.
+ *******************************************************************************/
+public class ConfigFilesBasedQQQApplication extends AbstractQQQApplication
+{
+ private final String path;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ConfigFilesBasedQQQApplication(String path)
+ {
+ this.path = path;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QInstance defineQInstance() throws QException
+ {
+ QInstance qInstance = new QInstance();
+ MetaDataLoaderHelper.processAllMetaDataFilesInDirectory(qInstance, path);
+ return (qInstance);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplication.java
new file mode 100644
index 00000000..91577042
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplication.java
@@ -0,0 +1,67 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.instances;
+
+
+/*******************************************************************************
+ ** Version of AbstractQQQApplication that assumes all meta-data is produced
+ ** by MetaDataProducers in (or under) a single package (where you can pass that
+ ** package into the constructor, vs. the abstract base class, where you extend
+ ** it and override the getMetaDataPackageName method.
+ *******************************************************************************/
+public class MetaDataProducerBasedQQQApplication extends AbstractMetaDataProducerBasedQQQApplication
+{
+ private final String metaDataPackageName;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public MetaDataProducerBasedQQQApplication(String metaDataPackageName)
+ {
+ this.metaDataPackageName = metaDataPackageName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public MetaDataProducerBasedQQQApplication(Class> aClassInMetaDataPackage)
+ {
+ this(aClassInMetaDataPackage.getPackageName());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public String getMetaDataPackageName()
+ {
+ return (this.metaDataPackageName);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
index eb54ff6b..6895a258 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java
@@ -330,7 +330,21 @@ public class QInstanceEnricher
if(table.getFields() != null)
{
- table.getFields().values().forEach(this::enrichField);
+ for(Map.Entry entry : table.getFields().entrySet())
+ {
+ String name = entry.getKey();
+ QFieldMetaData field = entry.getValue();
+
+ ////////////////////////////////////////////////////////////////////////////
+ // in case the field wasn't given a name, use its key from the fields map //
+ ////////////////////////////////////////////////////////////////////////////
+ if(!StringUtils.hasContent(field.getName()))
+ {
+ field.setName(name);
+ }
+
+ enrichField(field);
+ }
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values())
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
index c73f9b8e..8f7793ef 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
@@ -64,6 +64,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaDa
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
@@ -651,6 +652,8 @@ public class QInstanceValidator
validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class);
}
+ authentication.validate(qInstance, this);
+
runPlugins(QAuthenticationMetaData.class, authentication, qInstance);
}
}
@@ -1406,7 +1409,7 @@ public class QInstanceValidator
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
- Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass);
+ Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass, codeReference);
TableCustomizers tableCustomizer = TableCustomizers.forRole(roleName);
if(tableCustomizer == null)
@@ -1467,8 +1470,13 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
- private Object getInstanceOfCodeReference(String prefix, Class> clazz)
+ private Object getInstanceOfCodeReference(String prefix, Class> clazz, QCodeReference codeReference)
{
+ if(codeReference instanceof QCodeReferenceLambda> lambdaCodeReference)
+ {
+ return (lambdaCodeReference.getLambda());
+ }
+
Object instance = null;
try
{
@@ -1647,21 +1655,26 @@ public class QInstanceValidator
Set usedStepNames = new HashSet<>();
if(assertCondition(CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + "."))
{
- int index = 0;
+ int index = -1;
for(QStepMetaData step : process.getStepList())
{
+ index++;
if(assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName))
{
assertCondition(!usedStepNames.contains(step.getName()), "Duplicate step name [" + step.getName() + "] in process " + processName);
usedStepNames.add(step.getName());
}
- index++;
////////////////////////////////////////////
// validate instantiation of step classes //
////////////////////////////////////////////
if(step instanceof QBackendStepMetaData backendStepMetaData)
{
+ if(assertCondition(backendStepMetaData.getCode() != null, "Missing code for a backend step at index " + index + " in process " + processName))
+ {
+ validateSimpleCodeReference("Process " + processName + ", backend step at index " + index + ", code reference: ", backendStepMetaData.getCode(), BackendStep.class);
+ }
+
if(backendStepMetaData.getInputMetaData() != null && CollectionUtils.nullSafeHasContents(backendStepMetaData.getInputMetaData().getFieldList()))
{
for(QFieldMetaData fieldMetaData : backendStepMetaData.getInputMetaData().getFieldList())
@@ -2247,7 +2260,7 @@ public class QInstanceValidator
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
- Object classInstance = getInstanceOfCodeReference(prefix, clazz);
+ Object classInstance = getInstanceOfCodeReference(prefix, clazz, codeReference);
////////////////////////////////////////////////////////////////////////
// make sure the customizer instance can be cast to the expected type //
@@ -2270,6 +2283,11 @@ public class QInstanceValidator
Class> clazz = null;
try
{
+ if(codeReference instanceof QCodeReferenceLambda> lambdaCodeReference)
+ {
+ return (lambdaCodeReference.getLambda().getClass());
+ }
+
clazz = Class.forName(codeReference.getName());
}
catch(ClassNotFoundException e)
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java
new file mode 100644
index 00000000..ffd659a8
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java
@@ -0,0 +1,510 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import com.kingsrook.qqq.backend.core.utils.YamlUtils;
+import org.apache.commons.io.IOUtils;
+import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsInteger;
+import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsString;
+
+
+/*******************************************************************************
+ ** Abstract base class in hierarchy of classes that know how to construct &
+ ** populate QMetaDataObject instances, based on input streams (e.g., from files).
+ *******************************************************************************/
+public abstract class AbstractMetaDataLoader
+{
+ private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class);
+
+ private String fileName;
+
+ private List problems = new ArrayList<>();
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public T fileToMetaDataObject(QInstance qInstance, InputStream inputStream, String fileName) throws QMetaDataLoaderException
+ {
+ this.fileName = fileName;
+ Map map = fileToMap(inputStream, fileName);
+ LoadingContext loadingContext = new LoadingContext(fileName, "/");
+ return (mapToMetaDataObject(qInstance, map, loadingContext));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public abstract T mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ protected Map fileToMap(InputStream inputStream, String fileName) throws QMetaDataLoaderException
+ {
+ try
+ {
+ String string = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+ string = StringUtils.ltrim(string);
+ if(fileName.toLowerCase().endsWith(".json"))
+ {
+ return JsonUtils.toObject(string, new TypeReference<>() {});
+ }
+ else if(fileName.toLowerCase().endsWith(".yaml") || fileName.toLowerCase().endsWith(".yml"))
+ {
+ return YamlUtils.toMap(string);
+ }
+
+ throw (new QMetaDataLoaderException("Unsupported file format (based on file name: " + fileName + ")"));
+ }
+ catch(IOException e)
+ {
+ throw new QMetaDataLoaderException("Error building map from file: " + fileName, e);
+ }
+ }
+
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ protected void reflectivelyMap(QInstance qInstance, QMetaDataObject targetObject, Map map, LoadingContext context)
+ {
+ Class extends QMetaDataObject> targetClass = targetObject.getClass();
+ Set usedFieldNames = new HashSet<>();
+
+ for(Method method : targetClass.getMethods())
+ {
+ try
+ {
+ if(method.getName().startsWith("set") && method.getParameterTypes().length == 1)
+ {
+ String propertyName = StringUtils.lcFirst(method.getName().substring(3));
+
+ if(map.containsKey(propertyName))
+ {
+ usedFieldNames.add(propertyName);
+ Class> parameterType = method.getParameterTypes()[0];
+ Object rawValue = map.get(propertyName);
+
+ try
+ {
+ Object mappedValue = reflectivelyMapValue(qInstance, method, parameterType, rawValue, context.descendToProperty(propertyName));
+ method.invoke(targetObject, mappedValue);
+ }
+ catch(NoValueException nve)
+ {
+ ///////////////////////
+ // don't call setter //
+ ///////////////////////
+ LOG.debug("at " + context + ": No value was mapped for property [" + propertyName + "] on " + targetClass.getSimpleName() + "." + method.getName() + ", raw value: [" + rawValue + "]");
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ addProblem(new LoadingProblem(context, "Error reflectively mapping on " + targetClass.getName() + "." + method.getName(), e));
+ }
+ }
+
+ //////////////////////////
+ // mmm, slightly sus... //
+ //////////////////////////
+ map.remove("class");
+ map.remove("version");
+
+ Set unrecognizedKeys = new HashSet<>(map.keySet());
+ unrecognizedKeys.removeAll(usedFieldNames);
+
+ if(!unrecognizedKeys.isEmpty())
+ {
+ addProblem(new LoadingProblem(context, unrecognizedKeys.size() + " Unrecognized " + StringUtils.plural(unrecognizedKeys, "property", "properties") + ": " + unrecognizedKeys));
+ }
+ }
+
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ public Object reflectivelyMapValue(QInstance qInstance, Method method, Class> parameterType, Object rawValue, LoadingContext context) throws Exception
+ {
+ if(rawValue instanceof String s && s.matches("^\\$\\{.+\\..+}"))
+ {
+ rawValue = new QMetaDataVariableInterpreter().interpret(s);
+ LOG.debug("Interpreted raw value [" + s + "] as [" + StringUtils.maskAndTruncate(ValueUtils.getValueAsString(rawValue) + "]"));
+ }
+
+ if(parameterType.equals(String.class))
+ {
+ return (getValueAsString(rawValue));
+ }
+ else if(parameterType.equals(Integer.class))
+ {
+ try
+ {
+ return (getValueAsInteger(rawValue));
+ }
+ catch(Exception e)
+ {
+ addProblem(new LoadingProblem(context, "[" + rawValue + "] is not an Integer value."));
+ }
+ }
+ else if(parameterType.equals(Boolean.class))
+ {
+ if("true".equals(rawValue) || Boolean.TRUE.equals(rawValue))
+ {
+ return (true);
+ }
+ else if("false".equals(rawValue) || Boolean.FALSE.equals(rawValue))
+ {
+ return (false);
+ }
+ else if(rawValue == null)
+ {
+ return (null);
+ }
+ else
+ {
+ addProblem(new LoadingProblem(context, "[" + rawValue + "] is not a boolean value (must be 'true' or 'false')."));
+ return (null);
+ }
+ }
+ else if(parameterType.equals(boolean.class))
+ {
+ if("true".equals(rawValue) || Boolean.TRUE.equals(rawValue))
+ {
+ return (true);
+ }
+ else if("false".equals(rawValue) || Boolean.FALSE.equals(rawValue))
+ {
+ return (false);
+ }
+ else
+ {
+ addProblem(new LoadingProblem(context, rawValue + " is not a boolean value (must be 'true' or 'false')."));
+ throw (new NoValueException());
+ }
+ }
+ else if(parameterType.equals(List.class))
+ {
+ Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
+ Class> actualTypeClass = Class.forName(actualTypeArgument.getTypeName());
+
+ if(rawValue instanceof @SuppressWarnings("rawtypes")List valueList)
+ {
+ List mappedValueList = new ArrayList<>();
+ for(Object o : valueList)
+ {
+ try
+ {
+ Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o, context);
+ mappedValueList.add(mappedValue);
+ }
+ catch(NoValueException nve)
+ {
+ // leave off list
+ }
+ }
+ return (mappedValueList);
+ }
+ }
+ else if(parameterType.equals(Set.class))
+ {
+ Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
+ Class> actualTypeClass = Class.forName(actualTypeArgument.getTypeName());
+
+ if(rawValue instanceof @SuppressWarnings("rawtypes")List valueList)
+ {
+ Set mappedValueSet = new LinkedHashSet<>();
+ for(Object o : valueList)
+ {
+ try
+ {
+ Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o, context);
+ mappedValueSet.add(mappedValue);
+ }
+ catch(NoValueException nve)
+ {
+ // leave off list
+ }
+ }
+ return (mappedValueSet);
+ }
+ }
+ else if(parameterType.equals(Map.class))
+ {
+ Type keyType = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
+ if(!keyType.equals(String.class))
+ {
+ addProblem(new LoadingProblem(context, "Unsupported key type for " + method + " got [" + keyType + "], expected [String]"));
+ throw new NoValueException();
+ }
+ // todo make sure string
+
+ Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[1];
+ Class> actualTypeClass = Class.forName(actualTypeArgument.getTypeName());
+
+ if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap)
+ {
+ Map mappedValueMap = new LinkedHashMap<>();
+ for(Object o : valueMap.entrySet())
+ {
+ try
+ {
+ @SuppressWarnings("unchecked")
+ Map.Entry entry = (Map.Entry) o;
+ Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, entry.getValue(), context);
+ mappedValueMap.put(entry.getKey(), mappedValue);
+ }
+ catch(NoValueException nve)
+ {
+ // leave out of map
+ }
+ }
+ return (mappedValueMap);
+ }
+ }
+ else if(parameterType.isEnum())
+ {
+ String value = getValueAsString(rawValue);
+ for(Object enumConstant : parameterType.getEnumConstants())
+ {
+ if(((Enum>) enumConstant).name().equals(value))
+ {
+ return (enumConstant);
+ }
+ }
+
+ addProblem(new LoadingProblem(context, "Unrecognized value [" + rawValue + "]. Expected one of: " + Arrays.toString(parameterType.getEnumConstants())));
+ }
+ else if(MetaDataLoaderRegistry.hasLoaderForClass(parameterType))
+ {
+ if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap)
+ {
+ Class extends AbstractMetaDataLoader>> loaderClass = MetaDataLoaderRegistry.getLoaderForClass(parameterType);
+ AbstractMetaDataLoader> loader = loaderClass.getConstructor().newInstance();
+ //noinspection unchecked
+ return (loader.mapToMetaDataObject(qInstance, valueMap, context));
+ }
+ }
+ else if(QMetaDataObject.class.isAssignableFrom(parameterType))
+ {
+ if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap)
+ {
+ QMetaDataObject childObject = (QMetaDataObject) parameterType.getConstructor().newInstance();
+ //noinspection unchecked
+ reflectivelyMap(qInstance, childObject, valueMap, context);
+ return (childObject);
+ }
+ }
+ else if(parameterType.equals(Serializable.class))
+ {
+ if(rawValue instanceof String
+ || rawValue instanceof Integer
+ || rawValue instanceof BigDecimal
+ || rawValue instanceof Boolean
+ )
+ {
+ return rawValue;
+ }
+ }
+ else
+ {
+ // todo clean up this message/level
+ addProblem(new LoadingProblem(context, "No case for " + parameterType + " (arg to: " + method + ")"));
+ }
+
+ throw new NoValueException();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // unclear if the below is needed. if so, useful to not re-write, but is hurting test coverage, so zombie until used //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ ///***************************************************************************
+ // *
+ // ***************************************************************************/
+ //protected ListOfMapOrMapOfMap getListOfMapOrMapOfMap(Map map, String key)
+ //{
+ // if(map.containsKey(key))
+ // {
+ // if(map.get(key) instanceof List)
+ // {
+ // return (new ListOfMapOrMapOfMap((List>) map.get(key)));
+ // }
+ // else if(map.get(key) instanceof Map)
+ // {
+ // return (new ListOfMapOrMapOfMap((Map>) map.get(key)));
+ // }
+ // else
+ // {
+ // LOG.warn("Expected list or map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName()));
+ // }
+ // }
+
+ // return (null);
+ //}
+
+ ///***************************************************************************
+ // *
+ // ***************************************************************************/
+ //protected List> getListOfMap(Map map, String key)
+ //{
+ // if(map.containsKey(key))
+ // {
+ // if(map.get(key) instanceof List)
+ // {
+ // return (List>) map.get(key);
+ // }
+ // else
+ // {
+ // LOG.warn("Expected list under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName()));
+ // }
+ // }
+
+ // return (null);
+ //}
+
+ ///***************************************************************************
+ // *
+ // ***************************************************************************/
+ //protected Map> getMapOfMap(Map map, String key)
+ //{
+ // if(map.containsKey(key))
+ // {
+ // if(map.get(key) instanceof Map)
+ // {
+ // return (Map>) map.get(key);
+ // }
+ // else
+ // {
+ // LOG.warn("Expected map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName()));
+ // }
+ // }
+
+ // return (null);
+ //}
+
+ ///***************************************************************************
+ // **
+ // ***************************************************************************/
+ //protected record ListOfMapOrMapOfMap(List> listOf, Map> mapOf)
+ //{
+ // /*******************************************************************************
+ // ** Constructor
+ // **
+ // *******************************************************************************/
+ // public ListOfMapOrMapOfMap(List> listOf)
+ // {
+ // this(listOf, null);
+ // }
+
+ // /*******************************************************************************
+ // ** Constructor
+ // **
+ // *******************************************************************************/
+ // public ListOfMapOrMapOfMap(Map> mapOf)
+ // {
+ // this(null, mapOf);
+ // }
+ //}
+
+
+
+ /*******************************************************************************
+ ** Getter for fileName
+ **
+ *******************************************************************************/
+ public String getFileName()
+ {
+ return fileName;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private static class NoValueException extends Exception
+ {
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public NoValueException()
+ {
+ super("No value");
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public void addProblem(LoadingProblem problem)
+ {
+ problems.add(problem);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for problems
+ **
+ *******************************************************************************/
+ public List getProblems()
+ {
+ return (problems);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java
new file mode 100644
index 00000000..ab89d608
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java
@@ -0,0 +1,120 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders;
+
+
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import com.kingsrook.qqq.backend.core.instances.loaders.implementations.GenericMetaDataLoader;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import com.kingsrook.qqq.backend.core.utils.memoization.AnyKey;
+import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
+
+
+/*******************************************************************************
+ ** Generic implementation of AbstractMetaDataLoader, who "detects" the class
+ ** of meta data object to be created, then defers to an appropriate subclass
+ ** to do the work.
+ *******************************************************************************/
+public class ClassDetectingMetaDataLoader extends AbstractMetaDataLoader
+{
+ private static final Memoization>> memoizedMetaDataObjectClasses = new Memoization<>();
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ public AbstractMetaDataLoader> getLoaderForFile(InputStream inputStream, String fileName) throws QMetaDataLoaderException
+ {
+ Map map = fileToMap(inputStream, fileName);
+ return (getLoaderForMap(map));
+ }
+
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ public AbstractMetaDataLoader> getLoaderForMap(Map map) throws QMetaDataLoaderException
+ {
+ if(map.containsKey("class"))
+ {
+ String classProperty = ValueUtils.getValueAsString(map.get("class"));
+ try
+ {
+ if(MetaDataLoaderRegistry.hasLoaderForSimpleName(classProperty))
+ {
+ Class extends AbstractMetaDataLoader>> loaderClass = MetaDataLoaderRegistry.getLoaderForSimpleName(classProperty);
+ return (loaderClass.getConstructor().newInstance());
+ }
+ else
+ {
+ Optional>> metaDataClasses = memoizedMetaDataObjectClasses.getResult(AnyKey.getInstance(), k -> ClassPathUtils.getClassesContainingNameAndOfType("MetaData", QMetaDataObject.class));
+ if(metaDataClasses.isEmpty())
+ {
+ throw (new QMetaDataLoaderException("Could not get list of metaDataObjects from class loader"));
+ }
+
+ for(Class> c : metaDataClasses.get())
+ {
+ if(c.getSimpleName().equals(classProperty) && QMetaDataObject.class.isAssignableFrom(c))
+ {
+ @SuppressWarnings("unchecked")
+ Class extends QMetaDataObject> metaDataClass = (Class extends QMetaDataObject>) c;
+ return new GenericMetaDataLoader<>(metaDataClass);
+ }
+ }
+ }
+ throw new QMetaDataLoaderException("Unexpected class [" + classProperty + "] (not a QMetaDataObject; doesn't have a registered MetaDataLoader) specified in " + getFileName());
+ }
+ catch(QMetaDataLoaderException qmdle)
+ {
+ throw (qmdle);
+ }
+ catch(Exception e)
+ {
+ throw new QMetaDataLoaderException("Error handling class [" + classProperty + "] specified in " + getFileName(), e);
+ }
+ }
+ else
+ {
+ throw new QMetaDataLoaderException("Cannot detect meta-data type, because [class] attribute was not specified in file: " + getFileName());
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QMetaDataObject mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException
+ {
+ AbstractMetaDataLoader> loaderForMap = getLoaderForMap(map);
+ return loaderForMap.mapToMetaDataObject(qInstance, map, context);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingContext.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingContext.java
new file mode 100644
index 00000000..3b3b3eb1
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingContext.java
@@ -0,0 +1,38 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.instances.loaders;
+
+
+/*******************************************************************************
+ ** Record to track where loader objects are - e.g., what file they're on,
+ ** and at what property path within the file (e.g., helps report problems).
+ *******************************************************************************/
+public record LoadingContext(String fileName, String propertyPath)
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public LoadingContext descendToProperty(String propertyName)
+ {
+ return new LoadingContext(fileName, propertyPath + propertyName + "/");
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingProblem.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingProblem.java
new file mode 100644
index 00000000..ac783697
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingProblem.java
@@ -0,0 +1,49 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.instances.loaders;
+
+
+/*******************************************************************************
+ ** record that tracks a problem that was encountered when loading files.
+ *******************************************************************************/
+public record LoadingProblem(LoadingContext context, String message, Exception exception) // todo Level if useful
+{
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public LoadingProblem(LoadingContext context, String message)
+ {
+ this(context, message, null);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public String toString()
+ {
+ return "at[" + context.fileName() + "][" + context.propertyPath() + "]: " + message;
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java
new file mode 100644
index 00000000..54a78064
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java
@@ -0,0 +1,118 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders;
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.Pair;
+
+
+/*******************************************************************************
+ ** class that loads a directory full of meta data files into meta data objects,
+ ** and then sets all of them in a QInstance.
+ *******************************************************************************/
+public class MetaDataLoaderHelper
+{
+ private static final QLogger LOG = QLogger.getLogger(MetaDataLoaderHelper.class);
+
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ public static void processAllMetaDataFilesInDirectory(QInstance qInstance, String path) throws QException
+ {
+ List>> loaders = new ArrayList<>();
+
+ File directory = new File(path);
+ processAllMetaDataFilesInDirectory(loaders, directory);
+
+ // todo - some version of sorting the loaders by type or possibly a sort field within the files (or file names)
+
+ for(Pair> pair : loaders)
+ {
+ File file = pair.getA();
+ AbstractMetaDataLoader> loader = pair.getB();
+ try(FileInputStream fileInputStream = new FileInputStream(file))
+ {
+ QMetaDataObject qMetaDataObject = loader.fileToMetaDataObject(qInstance, fileInputStream, file.getName());
+
+ if(CollectionUtils.nullSafeHasContents(loader.getProblems()))
+ {
+ loader.getProblems().forEach(System.out::println);
+ }
+
+ if(qMetaDataObject instanceof TopLevelMetaDataInterface topLevelMetaData)
+ {
+ topLevelMetaData.addSelfToInstance(qInstance);
+ }
+ else
+ {
+ LOG.warn("Received a non-topLevelMetaDataObject from file: " + file.getAbsolutePath());
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.error("Error processing file: " + file.getAbsolutePath(), e);
+ }
+ }
+ }
+
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ private static void processAllMetaDataFilesInDirectory(List>> loaders, File directory) throws QException
+ {
+ for(File file : Objects.requireNonNullElse(directory.listFiles(), new File[0]))
+ {
+ if(file.isDirectory())
+ {
+ processAllMetaDataFilesInDirectory(loaders, file);
+ }
+ else
+ {
+ try(FileInputStream fileInputStream = new FileInputStream(file))
+ {
+ AbstractMetaDataLoader> loader = new ClassDetectingMetaDataLoader().getLoaderForFile(fileInputStream, file.getName());
+ loaders.add(Pair.of(file, loader));
+ }
+ catch(Exception e)
+ {
+ LOG.error("Error processing file: " + file.getAbsolutePath(), e);
+ }
+ }
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderRegistry.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderRegistry.java
new file mode 100644
index 00000000..069977fb
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderRegistry.java
@@ -0,0 +1,120 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.instances.loaders;
+
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class MetaDataLoaderRegistry
+{
+ private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class);
+
+ private static final Map, Class extends AbstractMetaDataLoader>>> registeredLoaders = new HashMap<>();
+ private static final Map>> registeredLoadersByTargetSimpleName = new HashMap<>();
+
+ static
+ {
+ try
+ {
+ List> classesInPackage = ClassPathUtils.getClassesInPackage(QTableMetaDataLoader.class.getPackageName());
+ for(Class> possibleLoaderClass : classesInPackage)
+ {
+ try
+ {
+ Type superClass = possibleLoaderClass.getGenericSuperclass();
+ if(superClass.getTypeName().startsWith(AbstractMetaDataLoader.class.getName() + "<"))
+ {
+ Type actualTypeArgument = ((ParameterizedType) superClass).getActualTypeArguments()[0];
+ if(actualTypeArgument instanceof Class)
+ {
+ //noinspection unchecked
+ Class extends AbstractMetaDataLoader>> loaderClass = (Class extends AbstractMetaDataLoader>>) possibleLoaderClass;
+
+ Class> metaDataObjectType = Class.forName(actualTypeArgument.getTypeName());
+ registeredLoaders.put(metaDataObjectType, loaderClass);
+ registeredLoadersByTargetSimpleName.put(metaDataObjectType.getSimpleName(), loaderClass);
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.info("Error on class: " + possibleLoaderClass, e);
+ }
+ }
+
+ System.out.println("Registered loaders: " + registeredLoadersByTargetSimpleName);
+ }
+ catch(Exception e)
+ {
+ LOG.error("Error in static init block for MetaDataLoaderRegistry", e);
+ }
+ }
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static boolean hasLoaderForClass(Class> metaDataClass)
+ {
+ return registeredLoaders.containsKey(metaDataClass);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Class extends AbstractMetaDataLoader>> getLoaderForClass(Class> metaDataClass)
+ {
+ return registeredLoaders.get(metaDataClass);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static boolean hasLoaderForSimpleName(String targetSimpleName)
+ {
+ return registeredLoadersByTargetSimpleName.containsKey(targetSimpleName);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Class extends AbstractMetaDataLoader>> getLoaderForSimpleName(String targetSimpleName)
+ {
+ return registeredLoadersByTargetSimpleName.get(targetSimpleName);
+ }
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java
new file mode 100644
index 00000000..24a3885d
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java
@@ -0,0 +1,50 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class QMetaDataLoaderException extends Exception
+{
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QMetaDataLoaderException(String message)
+ {
+ super(message);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public QMetaDataLoaderException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoader.java
new file mode 100644
index 00000000..9e5c5ce5
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoader.java
@@ -0,0 +1,71 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders.implementations;
+
+
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader;
+import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
+import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class GenericMetaDataLoader extends AbstractMetaDataLoader
+{
+ private final Class metaDataClass;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public GenericMetaDataLoader(Class metaDataClass)
+ {
+ this.metaDataClass = metaDataClass;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public T mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException
+ {
+ try
+ {
+ T object = metaDataClass.getConstructor().newInstance();
+ reflectivelyMap(qInstance, object, map, context);
+ return (object);
+ }
+ catch(Exception e)
+ {
+ throw (new QMetaDataLoaderException("Error loading metaData object of type " + metaDataClass.getSimpleName(), e));
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QStepDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QStepDataLoader.java
new file mode 100644
index 00000000..6333389b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QStepDataLoader.java
@@ -0,0 +1,85 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders.implementations;
+
+
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader;
+import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
+import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class QStepDataLoader extends AbstractMetaDataLoader
+{
+ private static final QLogger LOG = QLogger.getLogger(QStepDataLoader.class);
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QStepMetaData mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException
+ {
+ String stepType = ValueUtils.getValueAsString(map.get("stepType"));
+
+ if(!StringUtils.hasContent(stepType))
+ {
+ throw (new QMetaDataLoaderException("stepType was not specified for process step"));
+ }
+
+ QStepMetaData step;
+ if("backend".equalsIgnoreCase(stepType))
+ {
+ step = new QBackendStepMetaData();
+ reflectivelyMap(qInstance, step, map, context);
+ }
+ else if("frontend".equalsIgnoreCase(stepType))
+ {
+ step = new QFrontendStepMetaData();
+ reflectivelyMap(qInstance, step, map, context);
+ }
+ // todo - we have custom factory methods for this, so, maybe needs all custom loader?
+ // else if("stateMachine".equalsIgnoreCase(stepType))
+ // {
+ // step = new QStateMachineStep();
+ // reflectivelyMap(qInstance, step, map, context);
+ // }
+ else
+ {
+ throw (new QMetaDataLoaderException("Unsupported step stepType: " + stepType));
+ }
+
+ return (step);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java
new file mode 100644
index 00000000..c5a8d788
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java
@@ -0,0 +1,58 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders.implementations;
+
+
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader;
+import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
+import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class QTableMetaDataLoader extends AbstractMetaDataLoader
+{
+ private static final QLogger LOG = QLogger.getLogger(QTableMetaDataLoader.class);
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QTableMetaData mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException
+ {
+ QTableMetaData table = new QTableMetaData();
+
+ reflectivelyMap(qInstance, table, map, context);
+
+ // todo - handle QTableBackendDetails, based on backend's type
+
+ return (table);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/QProcessPayload.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/QProcessPayload.java
new file mode 100644
index 00000000..aad533bb
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/QProcessPayload.java
@@ -0,0 +1,200 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.model.actions.processes;
+
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField;
+import com.kingsrook.qqq.backend.core.utils.ListingHash;
+import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils;
+
+
+/*******************************************************************************
+ ** base-class for bean-like classes to represent the fields of a process.
+ ** similar in spirit to QRecordEntity, but for processes.
+ *******************************************************************************/
+public class QProcessPayload
+{
+ private static final QLogger LOG = QLogger.getLogger(QProcessPayload.class);
+
+ private static final ListingHash, QRecordEntityField> fieldMapping = new ListingHash<>();
+
+
+
+ /*******************************************************************************
+ ** Build an entity of this QRecord type from a QRecord
+ **
+ *******************************************************************************/
+ public static T fromProcessState(Class c, ProcessState processState) throws QException
+ {
+ try
+ {
+ T entity = c.getConstructor().newInstance();
+ entity.populateFromProcessState(processState);
+ return (entity);
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Error building process payload from state.", e));
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ protected void populateFromProcessState(ProcessState processState)
+ {
+ try
+ {
+ List fieldList = getFieldList(this.getClass());
+ // originalRecordValues = new HashMap<>();
+
+ for(QRecordEntityField qRecordEntityField : fieldList)
+ {
+ Serializable value = processState.getValues().get(qRecordEntityField.getFieldName());
+ Object typedValue = qRecordEntityField.convertValueType(value);
+ qRecordEntityField.getSetter().invoke(this, typedValue);
+ // originalRecordValues.put(qRecordEntityField.getFieldName(), value);
+ }
+
+ // for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
+ // {
+ // List associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
+ // if(associatedRecords == null)
+ // {
+ // qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
+ // }
+ // else
+ // {
+ // List associatedEntityList = new ArrayList<>();
+ // for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
+ // {
+ // associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
+ // }
+ // qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
+ // }
+ // }
+
+ // for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
+ // {
+ // List associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
+ // if(associatedRecords == null)
+ // {
+ // qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
+ // }
+ // else
+ // {
+ // List associatedEntityList = new ArrayList<>();
+ // for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
+ // {
+ // associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
+ // }
+ // qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
+ // }
+ // }
+ }
+ catch(Exception e)
+ {
+ throw (new QRuntimeException("Error building process payload from process state.", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Copy the values from this payload into the given process state.
+ ** ALL fields in the entity will be set in the process state.
+ **
+ *******************************************************************************/
+ public void toProcessState(ProcessState processState) throws QRuntimeException
+ {
+ try
+ {
+ for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
+ {
+ processState.getValues().put(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
+ }
+ }
+ catch(Exception e)
+ {
+ throw (new QRuntimeException("Error populating process state from process payload.", e));
+ }
+ }
+
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ public static Set> allowedFieldTypes()
+ {
+ HashSet> classes = new HashSet<>(ReflectiveBeanLikeClassUtils.defaultAllowedTypes());
+ classes.add(Map.class);
+ classes.add(List.class);
+ return (classes);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static List getFieldList(Class extends QProcessPayload> c)
+ {
+ if(!fieldMapping.containsKey(c))
+ {
+ List fieldList = new ArrayList<>();
+ for(Method possibleGetter : c.getMethods())
+ {
+ if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, false, allowedFieldTypes()))
+ {
+ Optional setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter);
+
+ if(setter.isPresent())
+ {
+ String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter);
+ fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), null));
+ }
+ else
+ {
+ LOG.debug("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter.");
+ }
+ }
+ }
+ fieldMapping.put(c, fieldList);
+ }
+ return (fieldMapping.get(c));
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java
index ca066eb3..dad1a92d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java
@@ -628,4 +628,15 @@ public class RunBackendStepInput extends AbstractActionInput
{
return (QContext.getQInstance().getProcess(getProcessName()));
}
+
+
+
+ /***************************************************************************
+ ** return a QProcessPayload subclass instance, with values populated from
+ ** the current process state.
+ ***************************************************************************/
+ public T getProcessPayload(Class payloadClass) throws QException
+ {
+ return QProcessPayload.fromProcessState(payloadClass, getProcessState());
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java
index ba6b87c5..5f212788 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java
@@ -445,4 +445,14 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment);
}
+
+
+ /***************************************************************************
+ ** Update the process state with values from the input processPayload
+ ** subclass instance.
+ ***************************************************************************/
+ public void setProcessPayload(QProcessPayload processPayload)
+ {
+ processPayload.toProcessState(getProcessState());
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
index dd5e7c8f..b5a17f84 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java
@@ -33,6 +33,7 @@ import java.util.Set;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -42,7 +43,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
*
*******************************************************************************/
@JsonDeserialize(using = QFilterCriteriaDeserializer.class)
-public class QFilterCriteria implements Serializable, Cloneable
+public class QFilterCriteria implements Serializable, Cloneable, QMetaDataObject
{
private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java
index 52eca98c..24a2c3f9 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java
@@ -23,13 +23,14 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
/*******************************************************************************
** Bean representing an element of a query order-by clause.
**
*******************************************************************************/
-public class QFilterOrderBy implements Serializable, Cloneable
+public class QFilterOrderBy implements Serializable, Cloneable, QMetaDataObject
{
private String fieldName;
private boolean isAscending = true;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
index 471bdcea..95137359 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java
@@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.FilterVariableExpression;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@@ -45,7 +46,7 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
* Full "filter" for a query - a list of criteria and order-bys
*
*******************************************************************************/
-public class QQueryFilter implements Serializable, Cloneable
+public class QQueryFilter implements Serializable, Cloneable, QMetaDataObject
{
private static final QLogger LOG = QLogger.getLogger(QQueryFilter.class);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java
index d18a9615..f681aa58 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java
@@ -49,6 +49,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
+import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -325,13 +326,13 @@ public abstract class QRecordEntity
List fieldList = new ArrayList<>();
for(Method possibleGetter : c.getMethods())
{
- if(isGetter(possibleGetter))
+ if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, true))
{
- Optional setter = getSetterForGetter(c, possibleGetter);
+ Optional setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter);
if(setter.isPresent())
{
- String fieldName = getFieldNameFromGetter(possibleGetter);
+ String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter);
Optional fieldAnnotation = getQFieldAnnotation(c, fieldName);
if(fieldAnnotation.isPresent())
@@ -378,19 +379,19 @@ public abstract class QRecordEntity
List associationList = new ArrayList<>();
for(Method possibleGetter : c.getMethods())
{
- if(isGetter(possibleGetter))
+ if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, true))
{
- Optional setter = getSetterForGetter(c, possibleGetter);
+ Optional setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter);
if(setter.isPresent())
{
- String fieldName = getFieldNameFromGetter(possibleGetter);
+ String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter);
Optional associationAnnotation = getQAssociationAnnotation(c, fieldName);
if(associationAnnotation.isPresent())
{
@SuppressWarnings("unchecked")
- Class extends QRecordEntity> listTypeParam = (Class extends QRecordEntity>) getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType());
+ Class extends QRecordEntity> listTypeParam = (Class extends QRecordEntity>) ReflectiveBeanLikeClassUtils.getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType());
associationList.add(new QRecordEntityAssociation(fieldName, possibleGetter, setter.get(), listTypeParam, associationAnnotation.orElse(null)));
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java
index a9999b25..91293930 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java
@@ -28,6 +28,7 @@ import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
+import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@@ -170,6 +171,11 @@ public class QRecordEntityField
{
return (ValueUtils.getValueAsByteArray(value));
}
+
+ if(type.equals(Map.class))
+ {
+ return (ValueUtils.getValueAsMap(value));
+ }
}
catch(Exception e)
{
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java
index 9ae00fbd..e8ecedf8 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java
@@ -28,6 +28,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
*******************************************************************************/
public enum QAuthenticationType
{
+ OAUTH2("OAuth2"),
AUTH_0("auth0"),
TABLE_BASED("tableBased"),
FULLY_ANONYMOUS("fullyAnonymous"),
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
index 8e4ce83f..7b1fe014 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
@@ -1250,7 +1250,7 @@ public class QInstance
{
this.supplementalMetaData = new HashMap<>();
}
- this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData);
+ this.supplementalMetaData.put(supplementalMetaData.getName(), supplementalMetaData);
return (this);
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java
new file mode 100644
index 00000000..aeec0e2b
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java
@@ -0,0 +1,34 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.model.metadata;
+
+
+import java.io.Serializable;
+
+
+/*******************************************************************************
+ ** interface common among all objects that can be considered qqq meta data -
+ ** e.g., stored in a QInstance.
+ *******************************************************************************/
+public interface QMetaDataObject extends Serializable
+{
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java
index 28ce1d40..c6acf7a8 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata;
+import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@@ -30,20 +31,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
** Base-class for instance-level meta-data defined by some supplemental module, etc,
** outside of qqq core
*******************************************************************************/
-public abstract class QSupplementalInstanceMetaData implements TopLevelMetaDataInterface
+public interface QSupplementalInstanceMetaData extends TopLevelMetaDataInterface
{
- /*******************************************************************************
- ** Getter for type
- *******************************************************************************/
- public abstract String getType();
-
-
-
/*******************************************************************************
**
*******************************************************************************/
- public void enrich(QTableMetaData table)
+ default void enrich(QTableMetaData table)
{
////////////////////////
// noop in base class //
@@ -55,7 +49,7 @@ public abstract class QSupplementalInstanceMetaData implements TopLevelMetaDataI
/*******************************************************************************
**
*******************************************************************************/
- public void validate(QInstance qInstance, QInstanceValidator validator)
+ default void validate(QInstance qInstance, QInstanceValidator validator)
{
////////////////////////
// noop in base class //
@@ -68,9 +62,33 @@ public abstract class QSupplementalInstanceMetaData implements TopLevelMetaDataI
**
*******************************************************************************/
@Override
- public void addSelfToInstance(QInstance qInstance)
+ default void addSelfToInstance(QInstance qInstance)
{
qInstance.withSupplementalMetaData(this);
}
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static S of(QInstance qInstance, String name)
+ {
+ return ((S) qInstance.getSupplementalMetaData(name));
+ }
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static S ofOrWithNew(QInstance qInstance, String name, Supplier supplier)
+ {
+ S s = (S) qInstance.getSupplementalMetaData(name);
+ if(s == null)
+ {
+ s = supplier.get();
+ s.addSelfToInstance(qInstance);
+ }
+ return (s);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java
index 116bae9e..4b7cc8a3 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java
@@ -26,7 +26,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
** Interface for meta-data classes that can be added directly (e.g, at the top
** level) to a QInstance (such as a QTableMetaData - not a QFieldMetaData).
*******************************************************************************/
-public interface TopLevelMetaDataInterface extends MetaDataProducerOutput
+public interface TopLevelMetaDataInterface extends MetaDataProducerOutput, QMetaDataObject
{
/*******************************************************************************
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java
index f7cf4bfa..657c7035 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java
@@ -22,10 +22,13 @@
package com.kingsrook.qqq.backend.core.model.metadata.audits;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+
+
/*******************************************************************************
**
*******************************************************************************/
-public class QAuditRules
+public class QAuditRules implements QMetaDataObject
{
private AuditLevel auditLevel;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java
new file mode 100644
index 00000000..13ac62b8
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java
@@ -0,0 +1,320 @@
+/*
+ * 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.model.metadata.authentication;
+
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
+import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.OAuth2AuthenticationModule;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+
+
+/*******************************************************************************
+ ** Meta-data to provide details of an OAuth2 Authentication module
+ *******************************************************************************/
+public class OAuth2AuthenticationMetaData extends QAuthenticationMetaData
+{
+ private String baseUrl;
+ private String tokenUrl;
+ private String clientId;
+ private String scopes;
+
+ private String userSessionTableName;
+ private String redirectStateTableName;
+
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // keep this secret, on the server - don't let it be serialized and sent to a client! //
+ ////////////////////////////////////////////////////////////////////////////////////////
+ @JsonIgnore
+ private String clientSecret;
+
+
+
+ /*******************************************************************************
+ ** Default Constructor.
+ *******************************************************************************/
+ public OAuth2AuthenticationMetaData()
+ {
+ super();
+ setType(QAuthenticationType.OAUTH2);
+
+ //////////////////////////////////////////////////////////
+ // ensure this module is registered with the dispatcher //
+ //////////////////////////////////////////////////////////
+ QAuthenticationModuleDispatcher.registerModule(QAuthenticationType.OAUTH2.getName(), OAuth2AuthenticationModule.class.getName());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void validate(QInstance qInstance, QInstanceValidator qInstanceValidator)
+ {
+ super.validate(qInstance, qInstanceValidator);
+
+ String prefix = "OAuth2AuthenticationMetaData (named '" + getName() + "'): ";
+
+ qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), prefix + "baseUrl must be set");
+ qInstanceValidator.assertCondition(StringUtils.hasContent(clientId), prefix + "clientId must be set");
+ qInstanceValidator.assertCondition(StringUtils.hasContent(clientSecret), prefix + "clientSecret must be set");
+ qInstanceValidator.assertCondition(StringUtils.hasContent(scopes), prefix + "scopes must be set");
+
+ if(qInstanceValidator.assertCondition(StringUtils.hasContent(userSessionTableName), prefix + "userSessionTableName must be set"))
+ {
+ qInstanceValidator.assertCondition(qInstance.getTable(userSessionTableName) != null, prefix + "userSessionTableName ('" + userSessionTableName + "') was not found in the instance");
+ }
+
+ if(qInstanceValidator.assertCondition(StringUtils.hasContent(redirectStateTableName), prefix + "redirectStateTableName must be set"))
+ {
+ qInstanceValidator.assertCondition(qInstance.getTable(redirectStateTableName) != null, prefix + "redirectStateTableName ('" + redirectStateTableName + "') was not found in the instance");
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter, override to help fluent flows
+ *******************************************************************************/
+ public OAuth2AuthenticationMetaData withBaseUrl(String baseUrl)
+ {
+ setBaseUrl(baseUrl);
+ return this;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for baseUrl
+ **
+ *******************************************************************************/
+ public String getBaseUrl()
+ {
+ return baseUrl;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for baseUrl
+ **
+ *******************************************************************************/
+ public void setBaseUrl(String baseUrl)
+ {
+ this.baseUrl = baseUrl;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter, override to help fluent flows
+ *******************************************************************************/
+ public OAuth2AuthenticationMetaData withClientId(String clientId)
+ {
+ setClientId(clientId);
+ return this;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for clientId
+ **
+ *******************************************************************************/
+ public String getClientId()
+ {
+ return clientId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for clientId
+ **
+ *******************************************************************************/
+ public void setClientId(String clientId)
+ {
+ this.clientId = clientId;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter, override to help fluent flows
+ *******************************************************************************/
+ public OAuth2AuthenticationMetaData withClientSecret(String clientSecret)
+ {
+ setClientSecret(clientSecret);
+ return this;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for clientSecret
+ **
+ *******************************************************************************/
+ public String getClientSecret()
+ {
+ return clientSecret;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for clientSecret
+ **
+ *******************************************************************************/
+ public void setClientSecret(String clientSecret)
+ {
+ this.clientSecret = clientSecret;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for tokenUrl
+ *******************************************************************************/
+ public String getTokenUrl()
+ {
+ return (this.tokenUrl);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for tokenUrl
+ *******************************************************************************/
+ public void setTokenUrl(String tokenUrl)
+ {
+ this.tokenUrl = tokenUrl;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for tokenUrl
+ *******************************************************************************/
+ public OAuth2AuthenticationMetaData withTokenUrl(String tokenUrl)
+ {
+ this.tokenUrl = tokenUrl;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userSessionTableName
+ *******************************************************************************/
+ public String getUserSessionTableName()
+ {
+ return (this.userSessionTableName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userSessionTableName
+ *******************************************************************************/
+ public void setUserSessionTableName(String userSessionTableName)
+ {
+ this.userSessionTableName = userSessionTableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userSessionTableName
+ *******************************************************************************/
+ public OAuth2AuthenticationMetaData withUserSessionTableName(String userSessionTableName)
+ {
+ this.userSessionTableName = userSessionTableName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for redirectStateTableName
+ *******************************************************************************/
+ public String getRedirectStateTableName()
+ {
+ return (this.redirectStateTableName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for redirectStateTableName
+ *******************************************************************************/
+ public void setRedirectStateTableName(String redirectStateTableName)
+ {
+ this.redirectStateTableName = redirectStateTableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for redirectStateTableName
+ *******************************************************************************/
+ public OAuth2AuthenticationMetaData withRedirectStateTableName(String redirectStateTableName)
+ {
+ this.redirectStateTableName = redirectStateTableName;
+ return (this);
+ }
+
+
+ /*******************************************************************************
+ ** Getter for scopes
+ *******************************************************************************/
+ public String getScopes()
+ {
+ return (this.scopes);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for scopes
+ *******************************************************************************/
+ public void setScopes(String scopes)
+ {
+ this.scopes = scopes;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for scopes
+ *******************************************************************************/
+ public OAuth2AuthenticationMetaData withScopes(String scopes)
+ {
+ this.scopes = scopes;
+ return (this);
+ }
+
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java
index c300e7fa..33a17a3b 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java
@@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.authentication;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonFilter;
+import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
@@ -225,4 +226,15 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface
return (this);
}
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public void validate(QInstance qInstance, QInstanceValidator qInstanceValidator)
+ {
+ //////////////////
+ // noop at base //
+ //////////////////
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java
index 5beba2ea..1f154390 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java
@@ -23,13 +23,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.code;
import java.io.Serializable;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
/*******************************************************************************
** Pointer to code to be ran by the qqq framework, e.g., for custom behavior -
** maybe process steps, maybe customization to a table, etc.
*******************************************************************************/
-public class QCodeReference implements Serializable, Cloneable
+public class QCodeReference implements Serializable, Cloneable, QMetaDataObject
{
private String name;
private QCodeType codeType;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java
index 0bbbb078..ce59465a 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java
@@ -40,11 +40,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -54,7 +56,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
** Meta-data to represent a single field in a table.
**
*******************************************************************************/
-public class QFieldMetaData implements Cloneable
+public class QFieldMetaData implements Cloneable, QMetaDataObject
{
private static final QLogger LOG = QLogger.getLogger(QFieldMetaData.class);
@@ -187,7 +189,7 @@ public class QFieldMetaData implements Cloneable
{
try
{
- this.name = QRecordEntity.getFieldNameFromGetter(getter);
+ this.name = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(getter);
this.type = QFieldType.fromClass(getter.getReturnType());
@SuppressWarnings("unchecked")
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/help/QHelpContent.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/help/QHelpContent.java
index f7bb173d..c99b9d1f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/help/QHelpContent.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/help/QHelpContent.java
@@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.help;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
/*******************************************************************************
@@ -41,7 +42,7 @@ import java.util.Set;
** May be dynamically added to meta-data via (non-meta-) data - see
** HelpContentMetaDataProvider and QInstanceHelpContentManager
*******************************************************************************/
-public class QHelpContent
+public class QHelpContent implements QMetaDataObject
{
private String content;
private HelpFormat format;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java
index 012ccc6c..b713009b 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java
@@ -22,11 +22,14 @@
package com.kingsrook.qqq.backend.core.model.metadata.layout;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+
+
/*******************************************************************************
** Interface shared by meta-data objects which can be placed into an App.
** e.g., Tables, Processes, and Apps themselves (since they can be nested)
*******************************************************************************/
-public interface QAppChildMetaData
+public interface QAppChildMetaData extends QMetaDataObject
{
/*******************************************************************************
**
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java
index ed311341..5dc89794 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java
@@ -24,12 +24,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout;
import java.util.ArrayList;
import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
/*******************************************************************************
** A section of apps/tables/processes - a logical grouping.
*******************************************************************************/
-public class QAppSection implements Cloneable
+public class QAppSection implements Cloneable, QMetaDataObject
{
private String name;
private String label;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java
index 71c26660..3d98ee63 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java
@@ -22,6 +22,9 @@
package com.kingsrook.qqq.backend.core.model.metadata.layout;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+
+
/*******************************************************************************
** Icon to show associated with an App, Table, Process, etc.
**
@@ -31,7 +34,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout;
** Future may allow something like a "namespace", and/or multiple icons for
** use in different frontends, etc.
*******************************************************************************/
-public class QIcon implements Cloneable
+public class QIcon implements Cloneable, QMetaDataObject
{
private String name;
private String path;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java
index 07dc0d55..d94b1e8e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java
@@ -22,13 +22,14 @@
package com.kingsrook.qqq.backend.core.model.metadata.permissions;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
**
*******************************************************************************/
-public class QPermissionRules implements Cloneable
+public class QPermissionRules implements Cloneable, QMetaDataObject
{
private PermissionLevel level;
private DenyBehavior denyBehavior;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java
index aa5a1f6e..35b8316c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java
@@ -25,12 +25,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.processes;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
/*******************************************************************************
** Definition of a UI component in a frontend process steps.
*******************************************************************************/
-public class QFrontendComponentMetaData
+public class QFrontendComponentMetaData implements QMetaDataObject
{
private QComponentType type;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java
index 21112267..baaa16ae 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java
@@ -327,12 +327,23 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
/*******************************************************************************
- ** Setter for stepList
+ ** Setter for stepList - note - calling this method ALSO overwrites the steps map!
**
*******************************************************************************/
public void setStepList(List stepList)
{
- this.stepList = stepList;
+ if(stepList == null)
+ {
+ this.stepList = null;
+ this.steps = null;
+ }
+ else
+ {
+ this.stepList = new ArrayList<>();
+ this.steps = new HashMap<>();
+ }
+
+ withStepList(stepList);
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java
index dd370142..6f47893e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java
@@ -185,4 +185,25 @@ public class QStateMachineStep extends QStepMetaData
return (rs);
}
+
+ /*******************************************************************************
+ ** Setter for subSteps
+ *******************************************************************************/
+ public void setSubSteps(List subSteps)
+ {
+ this.subSteps = subSteps;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for subSteps
+ *******************************************************************************/
+ public QStateMachineStep withSubSteps(List subSteps)
+ {
+ this.subSteps = subSteps;
+ return (this);
+ }
+
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStepMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStepMetaData.java
index d2d12545..52e7c624 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStepMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStepMetaData.java
@@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.serialization.QStepMetaDataDeserializer;
@@ -37,7 +38,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.serialization.QStepMetaData
**
*******************************************************************************/
@JsonDeserialize(using = QStepMetaDataDeserializer.class)
-public abstract class QStepMetaData
+public abstract class QStepMetaData implements QMetaDataObject
{
private String name;
private String label;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QScheduleMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QScheduleMetaData.java
index d474a013..efcbe2b6 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QScheduleMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QScheduleMetaData.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.scheduleing;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -35,7 +36,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
** same moment.
**
*******************************************************************************/
-public class QScheduleMetaData
+public class QScheduleMetaData implements QMetaDataObject
{
private String schedulerName;
private String description;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java
index 5ccd956e..2085d30e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java
@@ -22,11 +22,14 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+
+
/*******************************************************************************
** definition of a qqq table that is "associated" with another table, e.g.,
** managed along with it - such as child-records under a parent record.
*******************************************************************************/
-public class Association
+public class Association implements QMetaDataObject
{
private String name;
private String associatedTableName;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java
index 157d01db..df2fbe0a 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java
@@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.instances.QInstanceHelpContentManager;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
@@ -36,7 +37,7 @@ import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
** A section of fields - a logical grouping.
** TODO - this class should be named QTableSection!
*******************************************************************************/
-public class QFieldSection
+public class QFieldSection implements QMetaDataObject
{
private String name;
private String label;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java
index a2c1204c..8be2b1f1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java
@@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@@ -32,7 +33,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
** Definition of a Unique Key (or "Constraint", if you wanna use fancy words)
** on a QTable.
*******************************************************************************/
-public class UniqueKey
+public class UniqueKey implements QMetaDataObject
{
private List fieldNames;
private String label;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java
index e9f5d69e..e63b81d3 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java
@@ -22,11 +22,14 @@
package com.kingsrook.qqq.backend.core.model.metadata.tables.automation;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+
+
/*******************************************************************************
** Table-automation meta-data to define how this table's per-record automation
** status is tracked.
*******************************************************************************/
-public class AutomationStatusTracking
+public class AutomationStatusTracking implements QMetaDataObject
{
private AutomationStatusTrackingType type;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java
index 13a57662..ad87a133 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java
@@ -24,13 +24,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables.automation;
import java.util.ArrayList;
import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
/*******************************************************************************
** Details about how this table's record automations are set up.
*******************************************************************************/
-public class QTableAutomationDetails
+public class QTableAutomationDetails implements QMetaDataObject
{
private AutomationStatusTracking statusTracking;
private String providerName;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java
index b089661c..24745549 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java
@@ -25,13 +25,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables.automation;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Definition of a specific action to run against a table
*******************************************************************************/
-public class TableAutomationAction
+public class TableAutomationAction implements QMetaDataObject
{
private String name;
private TriggerEvent triggerEvent;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java
index 1adcc3a4..787da174 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java
@@ -22,10 +22,13 @@
package com.kingsrook.qqq.backend.core.model.session;
+import java.io.Serializable;
+
+
/*******************************************************************************
**
*******************************************************************************/
-public class QUser implements Cloneable
+public class QUser implements Cloneable, Serializable
{
private String idReference;
private String fullName;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java
index 28db59d6..c8c0b784 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java
@@ -25,15 +25,14 @@ package com.kingsrook.qqq.backend.core.modules.authentication;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
-import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
-import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.NotImplementedException;
+
/*******************************************************************************
** Interface that a QAuthenticationModule must implement.
**
@@ -82,12 +81,12 @@ public interface QAuthenticationModuleInterface
}
- /*******************************************************************************
+ /***************************************************************************
**
- *******************************************************************************/
- default String createAccessToken(QAuthenticationMetaData metaData, String clientId, String clientSecret) throws AccessTokenException
+ ***************************************************************************/
+ default String getLoginRedirectUrl(String originalUrl) throws QAuthenticationException
{
- throw (new NotImplementedException("The method createAccessToken() is not implemented in the class: " + this.getClass().getSimpleName()));
+ throw (new NotImplementedException("The method getLoginRedirectUrl() is not implemented in the authentication module: " + this.getClass().getSimpleName()));
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java
index 8b871ac1..25923209 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java
@@ -1020,7 +1020,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// decode the accessToken and make sure it is not expired //
////////////////////////////////////////////////////////////
boolean needNewToken = true;
- if(accessToken != null)
+ if(StringUtils.hasContent(accessToken))
{
DecodedJWT jwt = JWT.decode(accessToken);
String payload = jwt.getPayload();
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java
index 457f6bbb..519b609f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java
@@ -24,9 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
import java.util.Map;
import java.util.UUID;
-import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
-import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
@@ -77,15 +75,4 @@ public class FullyAnonymousAuthenticationModule implements QAuthenticationModule
return session != null;
}
-
-
- /*******************************************************************************
- ** Load an instance of the appropriate state provider
- **
- *******************************************************************************/
- public String createAccessToken(QAuthenticationMetaData metaData, String clientId, String clientSecret) throws AccessTokenException
- {
- return (TEST_ACCESS_TOKEN);
- }
-
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java
index c3459e72..4f0d5da1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java
@@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
import java.util.Map;
import java.util.UUID;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
@@ -45,8 +46,13 @@ public class MockAuthenticationModule implements QAuthenticationModuleInterface
**
*******************************************************************************/
@Override
- public QSession createSession(QInstance qInstance, Map context)
+ public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException
{
+ if("Deny".equalsIgnoreCase(context.get("accessToken")))
+ {
+ throw (new QAuthenticationException("Access denied (per accessToken requesting as such)"));
+ }
+
QUser qUser = new QUser();
qUser.setIdReference("User:" + (System.currentTimeMillis() % USER_ID_MODULO));
qUser.setFullName("John Smith");
@@ -80,4 +86,16 @@ public class MockAuthenticationModule implements QAuthenticationModuleInterface
return (true);
}
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public String getLoginRedirectUrl(String originalUrl)
+ {
+ return originalUrl + "?createMockSession=true";
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java
new file mode 100644
index 00000000..aa644b12
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java
@@ -0,0 +1,486 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.modules.authentication.implementations;
+
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.context.CapturedContext;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+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.authentication.OAuth2AuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
+import com.kingsrook.qqq.backend.core.model.session.QUser;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.AuthorizationGrant;
+import com.nimbusds.oauth2.sdk.ErrorObject;
+import com.nimbusds.oauth2.sdk.GeneralException;
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+import com.nimbusds.oauth2.sdk.TokenResponse;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
+import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
+import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.oauth2.sdk.id.State;
+import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
+import com.nimbusds.oauth2.sdk.token.AccessToken;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import org.json.JSONObject;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** Implementation of OAuth2 authentication.
+ *******************************************************************************/
+public class OAuth2AuthenticationModule implements QAuthenticationModuleInterface
+{
+ private static final QLogger LOG = QLogger.getLogger(OAuth2AuthenticationModule.class);
+
+ private static boolean mayMemoize = true;
+
+ private static final Memoization getAccessTokenFromSessionUUIDMemoization = new Memoization()
+ .withTimeout(Duration.of(1, ChronoUnit.MINUTES))
+ .withMaxSize(1000);
+
+ private static final Memoization oidcProviderMetadataMemoization = new Memoization()
+ .withMayStoreNullValues(false);
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException
+ {
+ try
+ {
+ OAuth2AuthenticationMetaData oauth2MetaData = (OAuth2AuthenticationMetaData) qInstance.getAuthentication();
+
+ if(context.containsKey("code") && context.containsKey("state"))
+ {
+ ///////////////////////////////////////////////////////////////////////
+ // handle a callback to initially auth a user for a traditional //
+ // (non-js) site - where the code & state params come to the backend //
+ ///////////////////////////////////////////////////////////////////////
+ AuthorizationCode code = new AuthorizationCode(context.get("code"));
+
+ /////////////////////////////////////////
+ // verify the state in our state table //
+ /////////////////////////////////////////
+ AtomicReference redirectUri = new AtomicReference<>(null);
+ QContext.withTemporaryContext(new CapturedContext(qInstance, new QSystemUserSession()), () ->
+ {
+ QRecord redirectStateRecord = GetAction.execute(oauth2MetaData.getRedirectStateTableName(), Map.of("state", context.get("state")));
+ if(redirectStateRecord == null)
+ {
+ throw (new QAuthenticationException("State not found"));
+ }
+ redirectUri.set(redirectStateRecord.getValueString("redirectUri"));
+ });
+
+ URI redirectURI = new URI(redirectUri.get());
+ ClientSecretBasic clientSecretBasic = new ClientSecretBasic(new ClientID(oauth2MetaData.getClientId()), new Secret(oauth2MetaData.getClientSecret()));
+ AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, redirectURI);
+
+ URI tokenEndpoint = getOIDCProviderMetadata(oauth2MetaData).getTokenEndpointURI();
+ Scope scope = new Scope(oauth2MetaData.getScopes());
+ TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientSecretBasic, codeGrant, scope);
+
+ return createSessionFromTokenRequest(tokenRequest);
+ }
+ else if(context.containsKey("code") && context.containsKey("redirectUri") && context.containsKey("codeVerifier"))
+ {
+ ////////////////////////////////////////////////////////////////////////////////
+ // handle a call down to this backend code to initially auth a user for an //
+ // SPA that received a code (where the javascript generated the codeVerifier) //
+ ////////////////////////////////////////////////////////////////////////////////
+ AuthorizationCode code = new AuthorizationCode(context.get("code"));
+ URI callback = new URI(context.get("redirectUri"));
+ CodeVerifier codeVerifier = new CodeVerifier(context.get("codeVerifier"));
+ AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, codeVerifier);
+
+ ClientID clientID = new ClientID(oauth2MetaData.getClientId());
+ Secret clientSecret = new Secret(oauth2MetaData.getClientSecret());
+ ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret);
+
+ URI tokenEndpoint = getOIDCProviderMetadata(oauth2MetaData).getTokenEndpointURI();
+ Scope scope = new Scope(oauth2MetaData.getScopes());
+ TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, scope);
+
+ return createSessionFromTokenRequest(tokenRequest);
+ }
+ else if(context.containsKey("sessionUUID") || context.containsKey("sessionId") || context.containsKey("uuid"))
+ {
+ //////////////////////////////////////////////////////////////////////
+ // handle a "normal" request, where we aren't opening a new session //
+ // per-se, but instead are looking for one in our userSession table //
+ //////////////////////////////////////////////////////////////////////
+ String uuid = Objects.requireNonNullElseGet(context.get("sessionUUID"), () ->
+ Objects.requireNonNullElseGet(context.get("sessionId"), () ->
+ context.get("uuid")));
+
+ String accessToken = getAccessTokenFromSessionUUID(uuid);
+ QSession session = createSessionFromToken(accessToken);
+ session.setUuid(uuid);
+
+ //////////////////////////////////////////////////////////////////
+ // todo - do we need to validate its age or ping the provider?? //
+ //////////////////////////////////////////////////////////////////
+
+ return (session);
+ }
+ else
+ {
+ String message = "Did not receive recognized values in context for creating session";
+ LOG.warn(message, logPair("contextKeys", context.keySet()));
+ throw (new QAuthenticationException(message));
+ }
+ }
+ catch(QAuthenticationException qae)
+ {
+ throw (qae);
+ }
+ catch(Exception e)
+ {
+ throw (new QAuthenticationException("Failed to create session (token)", e));
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private QSession createSessionFromTokenRequest(TokenRequest tokenRequest) throws ParseException, IOException, QException
+ {
+ TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send());
+
+ if(tokenResponse.indicatesSuccess())
+ {
+ AccessToken accessToken = tokenResponse.toSuccessResponse().getTokens().getAccessToken();
+
+ ////////////////////////////////////////////////////////////////////
+ // todo - do we want to try to do anything with a refresh token?? //
+ ////////////////////////////////////////////////////////////////////
+ // RefreshToken refreshToken = tokenResponse.toSuccessResponse().getTokens().getRefreshToken();
+
+ QSession session = createSessionFromToken(accessToken.getValue());
+ insertUserSession(accessToken.getValue(), session);
+ return (session);
+ }
+ else
+ {
+ ErrorObject errorObject = tokenResponse.toErrorResponse().getErrorObject();
+ LOG.info("Token request failed", logPair("code", errorObject.getCode()), logPair("description", errorObject.getDescription()));
+ throw (new QAuthenticationException(errorObject.getDescription()));
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean isSessionValid(QInstance instance, QSession session)
+ {
+ if(session instanceof QSystemUserSession)
+ {
+ return (true);
+ }
+
+ try
+ {
+ String accessToken = getAccessTokenFromSessionUUID(session.getUuid());
+ DecodedJWT jwt = JWT.decode(accessToken);
+ if(jwt.getExpiresAtAsInstant().isBefore(Instant.now()))
+ {
+ LOG.debug("accessToken is expired", logPair("sessionUUID", session.getUuid()));
+ return (false);
+ }
+
+ return true;
+ }
+ catch(QAuthenticationException e)
+ {
+ return (false);
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public String getLoginRedirectUrl(String originalUrl) throws QAuthenticationException
+ {
+ try
+ {
+ QInstance qInstance = QContext.getQInstance();
+ OAuth2AuthenticationMetaData oauth2MetaData = (OAuth2AuthenticationMetaData) qInstance.getAuthentication();
+ String authUrl = getOIDCProviderMetadata(oauth2MetaData).getAuthorizationEndpointURI().toString();
+
+ QTableMetaData stateTable = QContext.getQInstance().getTable(oauth2MetaData.getRedirectStateTableName());
+ if(stateTable == null)
+ {
+ throw (new QAuthenticationException("The table specified as the oauthRedirectStateTableName [" + oauth2MetaData.getRedirectStateTableName() + "] is not defined in the QInstance"));
+ }
+
+ ///////////////////////////////////////////////////////////////////
+ // generate a secure state, of either default length (32 bytes), //
+ // or at a size (base64 encoded) that fits in the state table //
+ ///////////////////////////////////////////////////////////////////
+ Integer stateStringLength = stateTable.getField("state").getMaxLength();
+ State state = stateStringLength == null ? new State(32) : new State((stateStringLength / 4) * 3);
+ String stateValue = state.getValue();
+
+ /////////////////////////////
+ // insert the state record //
+ /////////////////////////////
+ QContext.withTemporaryContext(new CapturedContext(qInstance, new QSystemUserSession()), () ->
+ {
+ QRecord insertedState = new InsertAction().execute(new InsertInput(oauth2MetaData.getRedirectStateTableName()).withRecord(new QRecord()
+ .withValue("state", stateValue)
+ .withValue("redirectUri", originalUrl))).getRecords().get(0);
+ if(CollectionUtils.nullSafeHasContents(insertedState.getErrors()))
+ {
+ throw (new QAuthenticationException("Error storing redirect state: " + insertedState.getErrorsAsString()));
+ }
+ });
+
+ return authUrl
+ + "?client_id=" + URLEncoder.encode(oauth2MetaData.getClientId(), StandardCharsets.UTF_8)
+ + "&redirect_uri=" + URLEncoder.encode(originalUrl, StandardCharsets.UTF_8)
+ + "&response_type=code"
+ + "&scope=" + URLEncoder.encode(oauth2MetaData.getScopes(), StandardCharsets.UTF_8)
+ + "&state=" + URLEncoder.encode(state.getValue(), StandardCharsets.UTF_8);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error getting login redirect url", e);
+ throw (new QAuthenticationException("Error getting login redirect url", e));
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private QSession createSessionFromToken(String accessToken) throws QException
+ {
+ DecodedJWT jwt = JWT.decode(accessToken);
+ Base64.Decoder decoder = Base64.getUrlDecoder();
+ String payloadString = new String(decoder.decode(jwt.getPayload()));
+ JSONObject payload = new JSONObject(payloadString);
+
+ QSession session = new QSession();
+ QUser user = new QUser();
+ session.setUser(user);
+
+ user.setFullName("Unknown");
+ String email = Objects.requireNonNullElseGet(payload.optString("email", null), () -> payload.optString("sub", null));
+ String name = payload.optString("name", email);
+
+ user.setIdReference(email);
+ user.setFullName(name);
+
+ ////////////////////////////////////////////////////////////
+ // todo wip - this needs to be much better standardized w/ fe //
+ ////////////////////////////////////////////////////////////
+ session.withValueForFrontend("user", new HashMap<>(Map.of("name", name, "email", email)));
+
+ return session;
+ }
+
+
+
+ /*******************************************************************************
+ ** Insert a session as a new record into userSession table
+ *******************************************************************************/
+ private void insertUserSession(String accessToken, QSession qSession) throws QException
+ {
+ CapturedContext capturedContext = QContext.capture();
+ try
+ {
+ QContext.init(capturedContext.qInstance(), new QSystemUserSession());
+
+ UserSession userSession = new UserSession()
+ .withUuid(qSession.getUuid())
+ .withUserId(qSession.getUser().getIdReference())
+ .withAccessToken(accessToken);
+
+ new InsertAction().execute(new InsertInput(UserSession.TABLE_NAME).withRecordEntity(userSession));
+ }
+ finally
+ {
+ QContext.init(capturedContext);
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QSession createAutomatedSessionForUser(QInstance qInstance, Serializable userId) throws QAuthenticationException
+ {
+ return QAuthenticationModuleInterface.super.createAutomatedSessionForUser(qInstance, userId);
+ }
+
+
+
+ /*******************************************************************************
+ ** Look up access_token from session UUID
+ **
+ *******************************************************************************/
+ private String getAccessTokenFromSessionUUID(String sessionUUID) throws QAuthenticationException
+ {
+ if(mayMemoize)
+ {
+ return getAccessTokenFromSessionUUIDMemoization.getResultThrowing(sessionUUID, (String x) ->
+ doGetAccessTokenFromSessionUUID(sessionUUID)
+ ).orElse(null);
+ }
+ else
+ {
+ return (doGetAccessTokenFromSessionUUID(sessionUUID));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String doGetAccessTokenFromSessionUUID(String sessionUUID) throws QAuthenticationException
+ {
+ String accessToken = null;
+ QSession beforeSession = QContext.getQSession();
+
+ try
+ {
+ QContext.setQSession(new QSystemUserSession());
+
+ ///////////////////////////////////////
+ // query for the user session record //
+ ///////////////////////////////////////
+ QRecord userSessionRecord = new GetAction().executeForRecord(new GetInput(UserSession.TABLE_NAME)
+ .withUniqueKey(Map.of("uuid", sessionUUID))
+ .withShouldMaskPasswords(false)
+ .withShouldOmitHiddenFields(false));
+
+ if(userSessionRecord != null)
+ {
+ accessToken = userSessionRecord.getValueString("accessToken");
+
+ ////////////////////////////////////////////////////////////
+ // decode the accessToken and make sure it is not expired //
+ ////////////////////////////////////////////////////////////
+ if(accessToken != null)
+ {
+ DecodedJWT jwt = JWT.decode(accessToken);
+ if(jwt.getExpiresAtAsInstant().isBefore(Instant.now()))
+ {
+ throw (new QAuthenticationException("accessToken is expired"));
+ }
+ }
+ }
+ }
+ catch(QAuthenticationException qae)
+ {
+ throw (qae);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error looking up userSession by sessionUUID", e);
+ throw (new QAuthenticationException("Error looking up userSession by sessionUUID", e));
+ }
+ finally
+ {
+ QContext.setQSession(beforeSession);
+ }
+
+ return (accessToken);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public boolean usesSessionIdCookie()
+ {
+ return (true);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private OIDCProviderMetadata getOIDCProviderMetadata(OAuth2AuthenticationMetaData oAuth2AuthenticationMetaData) throws GeneralException, IOException
+ {
+ return oidcProviderMetadataMemoization.getResult(oAuth2AuthenticationMetaData.getName(), (name ->
+ {
+ Issuer issuer = new Issuer(oAuth2AuthenticationMetaData.getBaseUrl());
+ OIDCProviderMetadata metadata = OIDCProviderMetadata.resolve(issuer);
+ return (metadata);
+ })).orElseThrow(() -> new GeneralException("Could not resolve OIDCProviderMetadata for " + oAuth2AuthenticationMetaData.getName()));
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java
index 541b7d69..6c069ef9 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java
@@ -406,6 +406,7 @@ public class TableBasedAuthenticationModule implements QAuthenticationModuleInte
qUser.setIdReference(userRecord.getValueString(metaData.getUserTableUsernameField()));
QSession qSession = new QSession();
+ qSession.setUuid(sessionUuid);
qSession.setIdReference(sessionUuid);
qSession.setUser(qUser);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/RedirectStateMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/RedirectStateMetaDataProducer.java
new file mode 100644
index 00000000..c8e4f73c
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/RedirectStateMetaDataProducer.java
@@ -0,0 +1,82 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2023. 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.modules.authentication.implementations.metadata;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
+import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
+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.fields.ValueTooLongBehavior;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
+
+
+/*******************************************************************************
+ ** Meta Data Producer for RedirectState table
+ *******************************************************************************/
+public class RedirectStateMetaDataProducer extends MetaDataProducer
+{
+ public static final String TABLE_NAME = "redirectState";
+
+ private final String backendName;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public RedirectStateMetaDataProducer(String backendName)
+ {
+ this.backendName = backendName;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QTableMetaData produce(QInstance qInstance) throws QException
+ {
+ QTableMetaData tableMetaData = new QTableMetaData()
+ .withName(TABLE_NAME)
+ .withBackendName(backendName)
+ .withRecordLabelFormat("%s")
+ .withRecordLabelFields("state")
+ .withPrimaryKeyField("id")
+ .withUniqueKey(new UniqueKey("state"))
+ .withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
+
+ .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
+ .withField(new QFieldMetaData("state", QFieldType.STRING).withIsEditable(false).withMaxLength(45).withBehavior(ValueTooLongBehavior.ERROR))
+ .withField(new QFieldMetaData("redirectUri", QFieldType.STRING).withIsEditable(false).withMaxLength(4096).withBehavior(ValueTooLongBehavior.ERROR))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false));
+
+ return tableMetaData;
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java
index 4b90666f..bcc42f2f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java
@@ -61,6 +61,39 @@ public class ClassPathUtils
+ /*******************************************************************************
+ ** from https://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection
+ **
+ *******************************************************************************/
+ public static List> getClassesContainingNameAndOfType(String nameContains, Class> type) throws IOException
+ {
+ List> classes = new ArrayList<>();
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+
+ for(ClassPath.ClassInfo info : getTopLevelClasses(loader))
+ {
+ try
+ {
+ if(info.getName().contains(nameContains))
+ {
+ Class> testClass = info.load();
+ if(type.isAssignableFrom(testClass))
+ {
+ classes.add(testClass);
+ }
+ }
+ }
+ catch(Throwable t)
+ {
+ // ignore - comes up for non-class entries, like module-info
+ }
+ }
+
+ return (classes);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ReflectiveBeanLikeClassUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ReflectiveBeanLikeClassUtils.java
new file mode 100644
index 00000000..94614769
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ReflectiveBeanLikeClassUtils.java
@@ -0,0 +1,188 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.utils;
+
+
+import java.lang.reflect.AnnotatedParameterizedType;
+import java.lang.reflect.AnnotatedType;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.data.QIgnore;
+import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
+
+
+/*******************************************************************************
+ ** Utilities for bean-like classes (e.g., QRecordEntity, QProcessPayload) that
+ ** use reflection to understand their bean-fields
+ *******************************************************************************/
+public class ReflectiveBeanLikeClassUtils
+{
+ private static final QLogger LOG = QLogger.getLogger(ReflectiveBeanLikeClassUtils.class);
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static String getFieldNameFromGetter(Method getter)
+ {
+ String nameWithoutGet = getter.getName().replaceFirst("^get", "");
+ if(nameWithoutGet.length() == 1)
+ {
+ return (nameWithoutGet.toLowerCase(Locale.ROOT));
+ }
+ return (nameWithoutGet.substring(0, 1).toLowerCase(Locale.ROOT) + nameWithoutGet.substring(1));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static boolean isGetter(Method method, boolean allowAssociations)
+ {
+ return isGetter(method, allowAssociations, defaultAllowedTypes());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static boolean isGetter(Method method, boolean allowAssociations, Collection> allowedTypes)
+ {
+ if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*"))
+ {
+ if(allowedTypes.contains(method.getReturnType()) || (allowAssociations && isSupportedAssociation(method.getReturnType(), method.getAnnotatedReturnType())))
+ {
+ return (true);
+ }
+ else
+ {
+ if(!method.getName().equals("getClass") && method.getAnnotation(QIgnore.class) == null)
+ {
+ LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported.");
+ }
+ }
+ }
+ return (false);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static Optional getSetterForGetter(Class> c, Method getter)
+ {
+ String setterName = getter.getName().replaceFirst("^get", "set");
+ for(Method method : c.getMethods())
+ {
+ if(method.getName().equals(setterName))
+ {
+ if(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(getter.getReturnType()))
+ {
+ return (Optional.of(method));
+ }
+ else
+ {
+ LOG.info("Method [" + method.getName() + "] looks like a setter for [" + getter.getName() + "], but its parameters, [" + Arrays.toString(method.getParameterTypes()) + "], don't match the getter's return type [" + getter.getReturnType() + "]");
+ }
+ }
+ }
+ return (Optional.empty());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Collection> defaultAllowedTypes()
+ {
+ /////////////////////////////////////////////
+ // note - this list has implications upon: //
+ // - QFieldType.fromClass //
+ // - QRecordEntityField.convertValueType //
+ /////////////////////////////////////////////
+ return (Set.of(String.class,
+ Integer.class,
+ Long.class,
+ int.class,
+ Boolean.class,
+ boolean.class,
+ BigDecimal.class,
+ Instant.class,
+ LocalDate.class,
+ LocalTime.class,
+ byte[].class));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static boolean isSupportedAssociation(Class> returnType, AnnotatedType annotatedType)
+ {
+ Class> listTypeParam = getListTypeParam(returnType, annotatedType);
+ return (listTypeParam != null && QRecordEntity.class.isAssignableFrom(listTypeParam));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static Class> getListTypeParam(Class> listType, AnnotatedType annotatedType)
+ {
+ if(listType.equals(List.class))
+ {
+ if(annotatedType instanceof AnnotatedParameterizedType apt)
+ {
+ AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments();
+ for(AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments)
+ {
+ Type type = annotatedActualTypeArgument.getType();
+ if(type instanceof Class> c)
+ {
+ return (c);
+ }
+ }
+ }
+ }
+
+ return (null);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java
index 313cb069..a29d9e24 100755
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java
@@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.utils;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -532,4 +533,59 @@ public class StringUtils
return base + " (1)";
}
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static String maskAndTruncate(String value)
+ {
+ return (maskAndTruncate(value, "** MASKED **", 6, 4));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static String maskAndTruncate(String value, String mask, int minLengthToMask, int charsToShowOnEnds)
+ {
+ if(!hasContent(value))
+ {
+ return ("");
+ }
+
+ if(value.length() < minLengthToMask || value.length() < 2 * charsToShowOnEnds)
+ {
+ return mask;
+ }
+
+ if(value.length() < charsToShowOnEnds * 3)
+ {
+ return (value.substring(0, charsToShowOnEnds) + mask);
+ }
+
+ return (value.substring(0, charsToShowOnEnds) + mask + value.substring(value.length() - charsToShowOnEnds));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static String nCopies(int n, String s)
+ {
+ return (nCopiesWithGlue(n, s, ""));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static String nCopiesWithGlue(int n, String s, String glue)
+ {
+ return (StringUtils.join(glue, Collections.nCopies(n, s)));
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java
index 1e791abd..4bdc405c 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.utils;
+import java.io.IOException;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -38,6 +39,7 @@ import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.List;
+import java.util.Map;
import java.util.TimeZone;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
@@ -1012,4 +1014,37 @@ public class ValueUtils
return defaultIfCannotInfer;
}
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static Map getValueAsMap(Serializable value)
+ {
+ if(value == null)
+ {
+ return (null);
+ }
+ else if(value instanceof Map, ?> map)
+ {
+ return (map);
+ }
+ else if(value instanceof String string && string.startsWith("{") && string.endsWith("}"))
+ {
+ try
+ {
+ Map map = JsonUtils.toObject(string, Map.class);
+ return (map);
+ }
+ catch(IOException e)
+ {
+ throw new QValueException("Error parsing string to map", e);
+ }
+ }
+ else
+ {
+ throw new QValueException("Unrecognized object type in getValueAsMap: " + value.getClass().getSimpleName());
+ }
+ }
}
diff --git a/qqq-backend-core/src/main/resources/log4j2.xml b/qqq-backend-core/src/main/resources/log4j2.xml
index df03564e..06e98981 100644
--- a/qqq-backend-core/src/main/resources/log4j2.xml
+++ b/qqq-backend-core/src/main/resources/log4j2.xml
@@ -24,6 +24,7 @@
+
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java
index d5321bcc..c854eed1 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java
@@ -43,6 +43,9 @@ class AbstractMetaDataProducerBasedQQQApplicationTest extends BaseTest
{
QInstance qInstance = new TestApplication().defineQInstance();
assertEquals(1, qInstance.getTables().size());
+ assertEquals("fromProducer", qInstance.getTables().get("fromProducer").getName());
+ assertEquals(1, qInstance.getProcesses().size());
+ assertEquals("fromProducer", qInstance.getProcesses().get("fromProducer").getName());
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplicationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplicationTest.java
new file mode 100644
index 00000000..2a22b7e2
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplicationTest.java
@@ -0,0 +1,66 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.instances;
+
+
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.instances.producers.TestMetaDataProducer;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for MetaDataProducerBasedQQQApplication
+ *******************************************************************************/
+class MetaDataProducerBasedQQQApplicationTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws QException
+ {
+ QInstance qInstance = new MetaDataProducerBasedQQQApplication(getClass().getPackage().getName() + ".producers").defineQInstance();
+ assertEquals(1, qInstance.getTables().size());
+ assertEquals("fromProducer", qInstance.getTables().get("fromProducer").getName());
+ assertEquals(1, qInstance.getProcesses().size());
+ assertEquals("fromProducer", qInstance.getProcesses().get("fromProducer").getName());
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testConstructorThatTakeClass() throws QException
+ {
+ QInstance qInstance = new MetaDataProducerBasedQQQApplication(TestMetaDataProducer.class).defineQInstance();
+ assertEquals(1, qInstance.getTables().size());
+ assertEquals("fromProducer", qInstance.getTables().get("fromProducer").getName());
+ assertEquals(1, qInstance.getProcesses().size());
+ assertEquals("fromProducer", qInstance.getProcesses().get("fromProducer").getName());
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoaderTest.java
new file mode 100644
index 00000000..fb45da10
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoaderTest.java
@@ -0,0 +1,136 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.instances.loaders;
+
+
+import java.nio.charset.StandardCharsets;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.instances.loaders.implementations.GenericMetaDataLoader;
+import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for AbstractMetaDataLoader
+ *******************************************************************************/
+class AbstractMetaDataLoaderTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testVariousPropertyTypes() throws QMetaDataLoaderException
+ {
+ QProcessMetaData process = new GenericMetaDataLoader<>(QProcessMetaData.class).fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: QProcessMetaData
+ version: 1
+ name: myProcess
+ tableName: someTable
+ maxInputRecords: 1
+ isHidden: true
+ """, StandardCharsets.UTF_8), "myProcess.yaml");
+
+ assertEquals("myProcess", process.getName());
+ assertEquals("someTable", process.getTableName());
+ assertEquals(1, process.getMaxInputRecords());
+ assertTrue(process.getIsHidden());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testProblems() throws QMetaDataLoaderException
+ {
+ {
+ QTableMetaDataLoader loader = new QTableMetaDataLoader();
+ loader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: QTableMetaData
+ version: 1.0
+ name: myTable
+ something: foo
+ isHidden: hi
+ icon:
+ name: account_tree
+ size: big
+ weight: bold
+ fields:
+ id:
+ type: number
+ uniqueKeys: sure!
+ """, StandardCharsets.UTF_8), "myTable.yaml");
+
+ for(LoadingProblem problem : loader.getProblems())
+ {
+ System.out.println(problem);
+ }
+ }
+
+ {
+ GenericMetaDataLoader loader = new GenericMetaDataLoader<>(QProcessMetaData.class);
+ loader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: QProcessMetaData
+ version: 1.0
+ name: myProcess
+ maxInputRecords: many
+ """, StandardCharsets.UTF_8), "myProcess.yaml");
+
+ for(LoadingProblem problem : loader.getProblems())
+ {
+ System.out.println(problem);
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testEnvironmentValues() throws QMetaDataLoaderException
+ {
+ System.setProperty("myProcess.tableName", "someTable");
+ System.setProperty("myProcess.maxInputRecords", "47");
+
+ GenericMetaDataLoader loader = new GenericMetaDataLoader<>(QProcessMetaData.class);
+ QProcessMetaData processMetaData = loader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: QProcessMetaData
+ version: 1.0
+ name: myProcess
+ tableName: ${prop.myProcess.tableName}
+ maxInputRecords: ${prop.myProcess.maxInputRecords}
+ """, StandardCharsets.UTF_8), "myProcess.yaml");
+
+ assertEquals("someTable", processMetaData.getTableName());
+ assertEquals(47, processMetaData.getMaxInputRecords());
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java
new file mode 100644
index 00000000..ea6cc23b
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java
@@ -0,0 +1,115 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders;
+
+
+import java.nio.charset.StandardCharsets;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for ClassDetectingMetaDataLoader
+ *******************************************************************************/
+class ClassDetectingMetaDataLoaderTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testBasicSuccess() throws QMetaDataLoaderException
+ {
+ QMetaDataObject qMetaDataObject = new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: QTableMetaData
+ version: 1
+ name: myTable
+ backendName: someBackend
+ """, StandardCharsets.UTF_8), "myTable.yaml");
+
+ assertThat(qMetaDataObject).isInstanceOf(QTableMetaData.class);
+ QTableMetaData qTableMetaData = (QTableMetaData) qMetaDataObject;
+ assertEquals("myTable", qTableMetaData.getName());
+ assertEquals("someBackend", qTableMetaData.getBackendName());
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testProcess() throws QMetaDataLoaderException
+ {
+ QMetaDataObject qMetaDataObject = new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: QProcessMetaData
+ version: 1
+ name: myProcess
+ tableName: someTable
+ """, StandardCharsets.UTF_8), "myProcess.yaml");
+
+ assertThat(qMetaDataObject).isInstanceOf(QProcessMetaData.class);
+ QProcessMetaData qProcessMetaData = (QProcessMetaData) qMetaDataObject;
+ assertEquals("myProcess", qProcessMetaData.getName());
+ assertEquals("someTable", qProcessMetaData.getTableName());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testUnknownClassFails()
+ {
+ assertThatThrownBy(() -> new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: ya whatever
+ version: 1
+ name: myTable
+ """, StandardCharsets.UTF_8), "whatever.yaml"))
+ .isInstanceOf(QMetaDataLoaderException.class)
+ .hasMessageContaining("Unexpected class");
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testMissingClassAttributeFails()
+ {
+ assertThatThrownBy(() -> new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ version: 1
+ name: myTable
+ """, StandardCharsets.UTF_8), "aTable.yaml"))
+ .isInstanceOf(QMetaDataLoaderException.class)
+ .hasMessageContaining("[class] attribute was not specified");
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelperTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelperTest.java
new file mode 100644
index 00000000..327e8aef
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelperTest.java
@@ -0,0 +1,111 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import org.apache.commons.io.FileUtils;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for MetaDataLoaderHelper
+ *******************************************************************************/
+class MetaDataLoaderHelperTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test() throws Exception
+ {
+ Path tempDirectory = Files.createTempDirectory(getClass().getSimpleName());
+
+ writeFile("myTable", ".yaml", tempDirectory, """
+ class: QTableMetaData
+ version: 1
+ name: myTable
+ label: This is My Table
+ primaryKeyField: id
+ fields:
+ id:
+ name: id
+ type: INTEGER
+ name:
+ name: name
+ type: STRING
+ createDate:
+ name: createDate
+ type: DATE_TIME
+ """);
+
+ writeFile("yourTable", ".yaml", tempDirectory, """
+ class: QTableMetaData
+ version: 1
+ name: yourTable
+ label: Someone else's table
+ primaryKeyField: id
+ fields:
+ id:
+ name: id
+ type: INTEGER
+ name:
+ name: name
+ type: STRING
+ """);
+
+ QInstance qInstance = new QInstance();
+ MetaDataLoaderHelper.processAllMetaDataFilesInDirectory(qInstance, tempDirectory.toFile().getAbsolutePath());
+
+ assertEquals(2, qInstance.getTables().size());
+
+ QTableMetaData myTable = qInstance.getTable("myTable");
+ assertEquals("This is My Table", myTable.getLabel());
+ assertEquals(3, myTable.getFields().size());
+ assertEquals("id", myTable.getField("id").getName());
+ assertEquals(QFieldType.INTEGER, myTable.getField("id").getType());
+
+ QTableMetaData yourTable = qInstance.getTable("yourTable");
+ assertEquals("Someone else's table", yourTable.getLabel());
+ assertEquals(2, yourTable.getFields().size());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ void writeFile(String prefix, String suffix, Path directory, String content) throws IOException
+ {
+ FileUtils.writeStringToFile(File.createTempFile(prefix, suffix, directory.toFile()), content, StandardCharsets.UTF_8);
+ }
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QProcessMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QProcessMetaDataLoaderTest.java
new file mode 100644
index 00000000..edde858d
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QProcessMetaDataLoaderTest.java
@@ -0,0 +1,103 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders;
+
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+
+/*******************************************************************************
+ ** Unit test for loading a QProcessMetaData (doesn't need its own loader yet,
+ ** but is still a valuable high-level test target).
+ *******************************************************************************/
+class QProcessMetaDataLoaderTest extends BaseTest
+{
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testYaml() throws QMetaDataLoaderException
+ {
+ ClassDetectingMetaDataLoader metaDataLoader = new ClassDetectingMetaDataLoader();
+ QProcessMetaData process = (QProcessMetaData) metaDataLoader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: QProcessMetaData
+ version: 1.0
+ name: myProcess
+ stepList:
+ - name: myBackendStep
+ stepType: backend
+ code:
+ name: com.kingsrook.test.processes.MyBackendStep
+ - name: myFrontendStep
+ stepType: frontend
+ components:
+ - type: HELP_TEXT
+ values:
+ foo: bar
+ - type: VIEW_FORM
+ viewFields:
+ - name: myField
+ type: STRING
+ - name: yourField
+ type: DATE
+ """, StandardCharsets.UTF_8), "myProcess.yaml");
+
+ CollectionUtils.nonNullList(metaDataLoader.getProblems()).forEach(System.out::println);
+
+ assertEquals("myProcess", process.getName());
+ assertEquals(2, process.getAllSteps().size());
+
+ QBackendStepMetaData myBackendStep = process.getBackendStep("myBackendStep");
+ assertNotNull(myBackendStep, "myBackendStep should not be null");
+ // todo - propagate this? assertEquals("myBackendStep", myBackendStep.getName());
+ assertEquals("com.kingsrook.test.processes.MyBackendStep", myBackendStep.getCode().getName());
+
+ QFrontendStepMetaData myFrontendStep = process.getFrontendStep("myFrontendStep");
+ assertNotNull(myFrontendStep, "myFrontendStep should not be null");
+ assertEquals(2, myFrontendStep.getComponents().size());
+ assertEquals(QComponentType.HELP_TEXT, myFrontendStep.getComponents().get(0).getType());
+ assertEquals(Map.of("foo", "bar"), myFrontendStep.getComponents().get(0).getValues());
+ assertEquals(QComponentType.VIEW_FORM, myFrontendStep.getComponents().get(1).getType());
+
+ assertEquals(2, myFrontendStep.getViewFields().size());
+ assertEquals("myField", myFrontendStep.getViewFields().get(0).getName());
+ assertEquals(QFieldType.STRING, myFrontendStep.getViewFields().get(0).getType());
+ assertEquals("yourField", myFrontendStep.getViewFields().get(1).getName());
+ assertEquals(QFieldType.DATE, myFrontendStep.getViewFields().get(1).getType());
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java
new file mode 100644
index 00000000..cef4d89b
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java
@@ -0,0 +1,202 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.loaders;
+
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
+import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
+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.TestUtils;
+import com.kingsrook.qqq.backend.core.utils.YamlUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+
+/*******************************************************************************
+ ** Unit test for QTableMetaDataLoader
+ *******************************************************************************/
+class QTableMetaDataLoaderTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ @Disabled("Not quite yet passing - is a good goal to get to though!")
+ void testToYaml() throws QMetaDataLoaderException
+ {
+ QTableMetaData expectedTable = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
+ String expectedYaml = YamlUtils.toYaml(expectedTable);
+ QTableMetaData actualTable = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(expectedYaml, StandardCharsets.UTF_8), "person.yaml");
+ String actualYaml = YamlUtils.toYaml(actualTable);
+ assertEquals(expectedYaml, actualYaml);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testYaml() throws QMetaDataLoaderException
+ {
+ QTableMetaData table = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: QTableMetaData
+ version: 1.0
+ name: myTable
+ icon:
+ name: account_tree
+ fields:
+ id:
+ name: id
+ type: INTEGER
+ name:
+ name: name
+ type: STRING
+ uniqueKeys:
+ - label: Name
+ fieldNames:
+ - name
+ associations:
+ - name: A1
+ associatedTableName: yourTable
+ joinName: myTableJoinYourTable
+ - name: A2
+ associatedTableName: theirTable
+ joinName: myTableJoinTheirTable
+ permissionRules:
+ level: READ_WRITE_PERMISSIONS
+ denyBehavior: HIDDEN
+ permissionBaseName: myTablePermissions
+ customPermissionChecker:
+ name: com.kingsrook.SomeChecker
+ codeType: JAVA
+ ## todo recordSecurityLocks
+ ## todo auditRules
+ ## todo backendDetails
+ ## todo automationDetails
+ sections:
+ - name: identity
+ label: Identity
+ icon:
+ name: badge
+ tier: T1
+ fieldNames:
+ - id
+ - firstName
+ - lastName
+ customizers:
+ postQueryRecord:
+ name: com.kingsrook.SomePostQuery
+ codeType: JAVA
+ preDeleteRecord:
+ name: com.kingsrook.SomePreDelete
+ codeType: JAVA
+ disabledCapabilities:
+ - TABLE_COUNT
+ - QUERY_STATS
+ """, StandardCharsets.UTF_8), "myTable.yaml");
+
+ assertEquals("myTable", table.getName());
+
+ assertEquals(2, table.getFields().size());
+ // assertEquals("id", table.getFields().get("id").getName());
+ assertEquals(QFieldType.INTEGER, table.getFields().get("id").getType());
+ // assertEquals("name", table.getFields().get("name").getName());
+ assertEquals(QFieldType.STRING, table.getFields().get("name").getType());
+
+ assertNotNull(table.getIcon());
+ assertEquals("account_tree", table.getIcon().getName());
+
+ assertEquals(1, table.getUniqueKeys().size());
+ assertEquals(List.of("name"), table.getUniqueKeys().get(0).getFieldNames());
+ assertEquals("Name", table.getUniqueKeys().get(0).getLabel());
+
+ assertEquals(2, table.getAssociations().size());
+ assertEquals("A1", table.getAssociations().get(0).getName());
+ assertEquals("theirTable", table.getAssociations().get(1).getAssociatedTableName());
+
+ assertNotNull(table.getPermissionRules());
+ assertEquals(PermissionLevel.READ_WRITE_PERMISSIONS, table.getPermissionRules().getLevel());
+ assertEquals(DenyBehavior.HIDDEN, table.getPermissionRules().getDenyBehavior());
+ assertEquals("myTablePermissions", table.getPermissionRules().getPermissionBaseName());
+ assertNotNull(table.getPermissionRules().getCustomPermissionChecker());
+ assertEquals("com.kingsrook.SomeChecker", table.getPermissionRules().getCustomPermissionChecker().getName());
+ assertEquals(QCodeType.JAVA, table.getPermissionRules().getCustomPermissionChecker().getCodeType());
+
+ assertEquals(1, table.getSections().size());
+ assertEquals("identity", table.getSections().get(0).getName());
+ assertEquals(Tier.T1, table.getSections().get(0).getTier());
+ assertEquals(List.of("id", "firstName", "lastName"), table.getSections().get(0).getFieldNames());
+
+ assertEquals(2, table.getCustomizers().size());
+ assertEquals("com.kingsrook.SomePostQuery", table.getCustomizers().get(TableCustomizers.POST_QUERY_RECORD.getRole()).getName());
+ assertEquals("com.kingsrook.SomePreDelete", table.getCustomizers().get(TableCustomizers.PRE_DELETE_RECORD.getRole()).getName());
+
+ assertEquals(Set.of(Capability.TABLE_COUNT, Capability.QUERY_STATS), table.getDisabledCapabilities());
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSimpleJson() throws QMetaDataLoaderException
+ {
+ QTableMetaData table = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ {
+ "class": "QTableMetaData",
+ "version": "1.0",
+ "name": "myTable",
+ "fields":
+ {
+ "id": {"name": "id", "type": "INTEGER"},
+ "name": {"name": "name", "type": "STRING"}
+ }
+ }
+ """, StandardCharsets.UTF_8), "myTable.json");
+
+ assertEquals("myTable", table.getName());
+ assertEquals(2, table.getFields().size());
+ assertEquals("id", table.getFields().get("id").getName());
+ assertEquals(QFieldType.INTEGER, table.getFields().get("id").getType());
+ assertEquals("name", table.getFields().get("name").getName());
+ assertEquals(QFieldType.STRING, table.getFields().get("name").getType());
+ }
+
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoaderTest.java
new file mode 100644
index 00000000..19721921
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoaderTest.java
@@ -0,0 +1,81 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.instances.loaders.implementations;
+
+
+import java.nio.charset.StandardCharsets;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
+import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
+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.processes.QProcessMetaData;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for GenericMetaDataLoader - providing coverage for AbstractMetaDataLoader.
+ *******************************************************************************/
+class GenericMetaDataLoaderTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testProcess() throws QMetaDataLoaderException
+ {
+ ////////////////////////////////////////////////////////////////////////////////
+ // trying to get some coverage of various types in here (for Abstract loader) //
+ ////////////////////////////////////////////////////////////////////////////////
+ QProcessMetaData process = new GenericMetaDataLoader<>(QProcessMetaData.class).fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
+ class: QProcessMetaData
+ version: 1
+ name: myProcess
+ tableName: someTable
+ maxInputRecords: 1
+ isHidden: true
+ """, StandardCharsets.UTF_8), "myProcess.yaml");
+
+ assertEquals("myProcess", process.getName());
+ assertEquals("someTable", process.getTableName());
+ assertEquals(1, process.getMaxInputRecords());
+ assertTrue(process.getIsHidden());
+ }
+
+
+
+ /*******************************************************************************
+ ** just here for coverage of this class, as we're failing to hit it otherwise.
+ *******************************************************************************/
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Test
+ void testNoValueException()
+ {
+ assertThatThrownBy(() -> new GenericMetaDataLoader(QBackendMetaData.class).reflectivelyMapValue(new QInstance(), null, GenericMetaDataLoaderTest.class, "rawValue", new LoadingContext("test.yaml", "/")));
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/subpackage/TestProcessMetaDataProducer.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/subpackage/TestProcessMetaDataProducer.java
new file mode 100644
index 00000000..5f7e6289
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/subpackage/TestProcessMetaDataProducer.java
@@ -0,0 +1,47 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. 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.instances.producers.subpackage;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TestProcessMetaDataProducer implements MetaDataProducerInterface
+{
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QProcessMetaData produce(QInstance qInstance) throws QException
+ {
+ return new QProcessMetaData().withName("fromProducer");
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java
index 9faaabb1..391c9a77 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java
@@ -368,4 +368,76 @@ class StringUtilsTest extends BaseTest
assertTrue(StringUtils.safeEqualsIgnoreCase("timothy d. chamberlain", "timothy d. chamberlain"));
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNCopies()
+ {
+ assertEquals("", StringUtils.nCopies(0, "a"));
+ assertEquals("a", StringUtils.nCopies(1, "a"));
+ assertEquals("aa", StringUtils.nCopies(2, "a"));
+ assertEquals("ab ab ab ", StringUtils.nCopies(3, "ab "));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNCopiesWithGlue()
+ {
+ assertEquals("", StringUtils.nCopiesWithGlue(0, "a", ""));
+ assertEquals("", StringUtils.nCopiesWithGlue(0, "a", ","));
+ assertEquals("a", StringUtils.nCopiesWithGlue(1, "a", ","));
+ assertEquals("aa", StringUtils.nCopiesWithGlue(2, "a", ""));
+ assertEquals("a,a", StringUtils.nCopiesWithGlue(2, "a", ","));
+ assertEquals("ab ab ab", StringUtils.nCopiesWithGlue(3, "ab", " "));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testMaskAndTruncate()
+ {
+ assertEquals("", StringUtils.maskAndTruncate(null));
+ assertEquals("", StringUtils.maskAndTruncate(""));
+ assertEquals("** MASKED **", StringUtils.maskAndTruncate("1"));
+ assertEquals("** MASKED **", StringUtils.maskAndTruncate("12"));
+ assertEquals("** MASKED **", StringUtils.maskAndTruncate("123"));
+ assertEquals("** MASKED **", StringUtils.maskAndTruncate("1234"));
+ assertEquals("** MASKED **", StringUtils.maskAndTruncate("12345"));
+ assertEquals("** MASKED **", StringUtils.maskAndTruncate("123456"));
+ assertEquals("** MASKED **", StringUtils.maskAndTruncate("1234567"));
+ assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("12345678"));
+ assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("123456789"));
+ assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("1234567890"));
+ assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("12345678901"));
+ assertEquals("1234** MASKED **9012", StringUtils.maskAndTruncate("123456789012"));
+ assertEquals("1234** MASKED **6789", StringUtils.maskAndTruncate("123456789" + StringUtils.nCopies(100, "xyz") + "123456789"));
+
+ assertEquals("***", StringUtils.maskAndTruncate("12", "***", 3, 1));
+ assertEquals("1***3", StringUtils.maskAndTruncate("123", "***", 3, 1));
+ assertEquals("1***4", StringUtils.maskAndTruncate("1234", "***", 3, 1));
+ assertEquals("1***5", StringUtils.maskAndTruncate("12345", "***", 3, 1));
+ assertEquals("12***", StringUtils.maskAndTruncate("12345", "***", 3, 2));
+ assertEquals("12***56", StringUtils.maskAndTruncate("123456", "***", 3, 2));
+
+ assertEquals("***", StringUtils.maskAndTruncate("12", "***", 3, 4));
+ assertEquals("***", StringUtils.maskAndTruncate("123", "***", 3, 4));
+ assertEquals("***", StringUtils.maskAndTruncate("1234", "***", 3, 4));
+ assertEquals("***", StringUtils.maskAndTruncate("12345", "***", 3, 4));
+ assertEquals("***", StringUtils.maskAndTruncate("12345", "***", 3, 4));
+ assertEquals("***", StringUtils.maskAndTruncate("123456", "***", 3, 4));
+ assertEquals("1234***", StringUtils.maskAndTruncate("1234567890", "***", 3, 4));
+ assertEquals("1234***", StringUtils.maskAndTruncate("12345678901", "***", 3, 4));
+ assertEquals("1234***9012", StringUtils.maskAndTruncate("123456789012", "***", 3, 4));
+ }
+
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
index 54502074..731b7235 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
@@ -40,6 +40,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@@ -1289,7 +1290,7 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
- public static QSession getMockSession()
+ public static QSession getMockSession() throws QAuthenticationException
{
MockAuthenticationModule mockAuthenticationModule = new MockAuthenticationModule();
return (mockAuthenticationModule.createSession(null, null));
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java
index 2ea76133..767d2669 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java
@@ -34,7 +34,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
-public class ApiInstanceMetaDataContainer extends QSupplementalInstanceMetaData
+public class ApiInstanceMetaDataContainer implements QSupplementalInstanceMetaData
{
private Map apis;
@@ -71,17 +71,6 @@ public class ApiInstanceMetaDataContainer extends QSupplementalInstanceMetaData
- /*******************************************************************************
- **
- *******************************************************************************/
- @Override
- public String getType()
- {
- return (ApiSupplementType.NAME);
- }
-
-
-
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
index 80f227d3..5ed116b1 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
@@ -206,7 +206,7 @@ public class QJavalinImplementation
*******************************************************************************/
public QJavalinImplementation(QInstance qInstance) throws QInstanceValidationException
{
- this(qInstance, new QJavalinMetaData());
+ this(qInstance, QJavalinMetaData.ofOrWithNew(qInstance));
}
@@ -453,10 +453,21 @@ public class QJavalinImplementation
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
Map authContext = new HashMap<>();
- //? authContext.put("uuid", ValueUtils.getValueAsString(map.get("uuid")));
- authContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, ValueUtils.getValueAsString(map.get("accessToken")));
authContext.put(Auth0AuthenticationModule.DO_STORE_USER_SESSION_KEY, "true");
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // before this code iterated the map, it had zombied uuid line, and only actually used ACCESS_TOKEN_KEY //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //? authContext.put("uuid", ValueUtils.getValueAsString(map.get("uuid")));
+ // authContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, ValueUtils.getValueAsString(map.get("accessToken")));
+ /////////////////////////////////////////////////////////////////
+ // todo - have the auth module declare what values it expects? //
+ /////////////////////////////////////////////////////////////////
+ for(Map.Entry, ?> entry : map.entrySet())
+ {
+ authContext.put(ValueUtils.getValueAsString(entry.getKey()), ValueUtils.getValueAsString(entry.getValue()));
+ }
+
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// put the qInstance into context - but no session yet (since, the whole point of this call is to manage the session!) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -524,14 +535,24 @@ public class QJavalinImplementation
try
{
+ ///////////////////////////////////////////////
+ // note: duplicated in ExecutorSessionUtils //
+ ///////////////////////////////////////////////
Map authenticationContext = new HashMap<>();
String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY);
String authorizationHeaderValue = context.header("Authorization");
String apiKeyHeaderValue = context.header("x-api-key");
+ String codeQueryParamValue = context.queryParam("code");
+ String stateQueryParamValue = context.queryParam("state");
- if(StringUtils.hasContent(sessionIdCookieValue))
+ if(StringUtils.hasContent(codeQueryParamValue) && StringUtils.hasContent(stateQueryParamValue))
+ {
+ authenticationContext.put("code", codeQueryParamValue);
+ authenticationContext.put("state", stateQueryParamValue);
+ }
+ else if(StringUtils.hasContent(sessionIdCookieValue))
{
///////////////////////////////////////////////////////
// sessionId - maybe used by table-based auth module //
@@ -597,7 +618,7 @@ public class QJavalinImplementation
/////////////////////////////////////////////////////////////////////////////////
if(authenticationModule.usesSessionIdCookie())
{
- context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE);
+ context.cookie(SESSION_ID_COOKIE_NAME, session.getUuid(), SESSION_COOKIE_AGE);
}
setUserTimezoneOffsetMinutesInSession(context, session);
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java
index 89f422b2..3c1ea8df 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java
@@ -22,25 +22,66 @@
package com.kingsrook.qqq.backend.javalin;
+import java.util.ArrayList;
+import java.util.List;
import java.util.function.Function;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
+import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import org.apache.logging.log4j.Level;
/*******************************************************************************
** MetaData specific to a QQQ Javalin server.
*******************************************************************************/
-public class QJavalinMetaData
+public class QJavalinMetaData implements QSupplementalInstanceMetaData
{
+ public static final String NAME = "javalin";
+
private String uploadedFileArchiveTableName;
private boolean loggerDisabled = false;
+ // todo - should be a code reference!!
private Function logFilter;
private boolean queryWithoutLimitAllowed = false;
private Integer queryWithoutLimitDefault = 1000;
private Level queryWithoutLimitLogLevel = Level.INFO;
+ private List routeProviders;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public String getName()
+ {
+ return (NAME);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QJavalinMetaData of(QInstance qInstance)
+ {
+ return QSupplementalInstanceMetaData.of(qInstance, NAME);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static QJavalinMetaData ofOrWithNew(QInstance qInstance)
+ {
+ return QSupplementalInstanceMetaData.ofOrWithNew(qInstance, NAME, QJavalinMetaData::new);
+ }
+
/*******************************************************************************
@@ -241,4 +282,51 @@ public class QJavalinMetaData
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for routeProviders
+ *******************************************************************************/
+ public List getRouteProviders()
+ {
+ return (this.routeProviders);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for routeProviders
+ *******************************************************************************/
+ public void setRouteProviders(List routeProviders)
+ {
+ this.routeProviders = routeProviders;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for routeProviders
+ *******************************************************************************/
+ public QJavalinMetaData withRouteProviders(List routeProviders)
+ {
+ this.routeProviders = routeProviders;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter to add 1 routeProvider
+ *******************************************************************************/
+ public QJavalinMetaData withRouteProvider(JavalinRouteProviderMetaData routeProvider)
+ {
+ if(this.routeProviders == null)
+ {
+ this.routeProviders = new ArrayList<>();
+ }
+ this.routeProviders.add(routeProvider);
+ return (this);
+ }
+
+
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java
index ce720a1c..b0ed8bce 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java
@@ -22,6 +22,8 @@
package com.kingsrook.qqq.middleware.javalin;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@@ -31,12 +33,17 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.backend.javalin.QJavalinMetaData;
+import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
+import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouter;
+import com.kingsrook.qqq.middleware.javalin.routeproviders.SimpleFileSystemDirectoryRouter;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import io.javalin.Javalin;
+import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.Context;
import org.apache.commons.lang.BooleanUtils;
import org.eclipse.jetty.util.resource.Resource;
@@ -70,8 +77,8 @@ public class QApplicationJavalinServer
private boolean serveLegacyUnversionedMiddlewareAPI = true;
private List middlewareVersionList = List.of(new MiddlewareVersionV1());
private List additionalRouteProviders = null;
- private Consumer javalinConfigurationCustomizer = null;
- private QJavalinMetaData javalinMetaData = null;
+ private Consumer javalinConfigurationCustomizer = null;
+ private QJavalinMetaData javalinMetaData = null;
private long lastQInstanceHotSwapMillis;
private long millisBetweenHotSwaps = 2500;
@@ -99,6 +106,12 @@ public class QApplicationJavalinServer
{
QInstance qInstance = application.defineValidatedQInstance();
+ QJavalinMetaData qJavalinMetaData = QJavalinMetaData.of(qInstance);
+ if(qJavalinMetaData != null)
+ {
+ addRouteProvidersFromMetaData(qJavalinMetaData);
+ }
+
service = Javalin.create(config ->
{
if(serveFrontendMaterialDashboard)
@@ -117,7 +130,7 @@ public class QApplicationJavalinServer
////////////////////////////////////////////////////////////////////////////////////////
try(Resource resource = Resource.newClassPathResource("/material-dashboard-overlay"))
{
- if(resource !=null)
+ if(resource != null)
{
config.staticFiles.add("/material-dashboard-overlay");
}
@@ -173,10 +186,26 @@ public class QApplicationJavalinServer
for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
{
routeProvider.setQInstance(qInstance);
- config.router.apiBuilder(routeProvider.getJavalinEndpointGroup());
+
+ EndpointGroup javalinEndpointGroup = routeProvider.getJavalinEndpointGroup();
+ if(javalinEndpointGroup != null)
+ {
+ config.router.apiBuilder(javalinEndpointGroup);
+ }
+
+ routeProvider.acceptJavalinConfig(config);
}
});
+ //////////////////////////////////////////////////////////////////////
+ // also pass the javalin service into any additionalRouteProviders, //
+ // in case they need additional setup, e.g., before/after handlers. //
+ //////////////////////////////////////////////////////////////////////
+ for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
+ {
+ routeProvider.acceptJavalinService(service);
+ }
+
//////////////////////////////////////////////////////////////////////////////////////
// per system property, set the server to hot-swap the q instance before all routes //
//////////////////////////////////////////////////////////////////////////////////////
@@ -190,6 +219,8 @@ public class QApplicationJavalinServer
service.before((Context context) -> context.header("Content-Type", "application/json"));
service.after(QJavalinImplementation::clearQContext);
+ addNullResponseCharsetFixer();
+
////////////////////////////////////////////////
// allow a configuration-customizer to be run //
////////////////////////////////////////////////
@@ -203,6 +234,57 @@ public class QApplicationJavalinServer
+ /***************************************************************************
+ ** initial tests with the SimpleFileSystemDirectoryRouter would sometimes
+ ** have a Content-Type:text/html;charset=null !
+ ** which doesn't seem ever valid (and at least it broke our unit test).
+ ** so, if w see charset=null in contentType, replace it with the system
+ ** default, which may not be 100% right, but has to be better than "null"...
+ ***************************************************************************/
+ private void addNullResponseCharsetFixer()
+ {
+ service.after((Context context) ->
+ {
+ String contentType = context.res().getContentType();
+ if(contentType != null && contentType.contains("charset=null"))
+ {
+ contentType = contentType.replace("charset=null", "charset=" + Charset.defaultCharset().name());
+ context.res().setContentType(contentType);
+ }
+ });
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void addRouteProvidersFromMetaData(QJavalinMetaData qJavalinMetaData) throws QException
+ {
+ if(qJavalinMetaData == null)
+ {
+ return;
+ }
+
+ for(JavalinRouteProviderMetaData routeProviderMetaData : CollectionUtils.nonNullList(qJavalinMetaData.getRouteProviders()))
+ {
+ if(StringUtils.hasContent(routeProviderMetaData.getProcessName()) && StringUtils.hasContent(routeProviderMetaData.getHostedPath()))
+ {
+ withAdditionalRouteProvider(new ProcessBasedRouter(routeProviderMetaData));
+ }
+ else if(StringUtils.hasContent(routeProviderMetaData.getFileSystemPath()) && StringUtils.hasContent(routeProviderMetaData.getHostedPath()))
+ {
+ withAdditionalRouteProvider(new SimpleFileSystemDirectoryRouter(routeProviderMetaData));
+ }
+ else
+ {
+ throw (new QException("Error processing route provider - does not have sufficient fields set."));
+ }
+ }
+ }
+
+
+
/***************************************************************************
**
***************************************************************************/
@@ -459,6 +541,21 @@ public class QApplicationJavalinServer
+ /*******************************************************************************
+ ** Fluent setter to add a single additionalRouteProvider
+ *******************************************************************************/
+ public QApplicationJavalinServer withAdditionalRouteProvider(QJavalinRouteProviderInterface additionalRouteProvider)
+ {
+ if(this.additionalRouteProviders == null)
+ {
+ this.additionalRouteProviders = new ArrayList<>();
+ }
+ this.additionalRouteProviders.add(additionalRouteProvider);
+ return (this);
+ }
+
+
+
/*******************************************************************************
** Getter for MILLIS_BETWEEN_HOT_SWAPS
*******************************************************************************/
@@ -541,6 +638,7 @@ public class QApplicationJavalinServer
}
+
/*******************************************************************************
** Getter for javalinMetaData
*******************************************************************************/
@@ -570,5 +668,4 @@ public class QApplicationJavalinServer
return (this);
}
-
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java
index dabe521c..c6dbf40b 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java
@@ -23,7 +23,9 @@ package com.kingsrook.qqq.middleware.javalin;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import io.javalin.Javalin;
import io.javalin.apibuilder.EndpointGroup;
+import io.javalin.config.JavalinConfig;
/*******************************************************************************
@@ -42,6 +44,37 @@ public interface QJavalinRouteProviderInterface
/***************************************************************************
**
***************************************************************************/
- EndpointGroup getJavalinEndpointGroup();
+ default EndpointGroup getJavalinEndpointGroup()
+ {
+ /////////////////////////////
+ // no endpoints at default //
+ /////////////////////////////
+ return (null);
+ }
+
+
+ /***************************************************************************
+ ** when the javalin service is being configured as part of its boot up,
+ ** accept the javalinConfig object, to perform whatever setup you need,
+ ** such as setting up routes.
+ ***************************************************************************/
+ default void acceptJavalinConfig(JavalinConfig config)
+ {
+ /////////////////////
+ // noop at default //
+ /////////////////////
+ }
+
+ /***************************************************************************
+ ** when the javalin service is being configured as part of its boot up,
+ ** accept the Javalin service object, to perform whatever setup you need,
+ ** such as setting up before/after handlers.
+ ***************************************************************************/
+ default void acceptJavalinService(Javalin service)
+ {
+ /////////////////////
+ // noop at default //
+ /////////////////////
+ }
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java
index 7a379fd5..db6797d0 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java
@@ -60,14 +60,24 @@ public class ExecutorSessionUtils
try
{
+ /////////////////////////////////////////////////
+ // note: duplicated in QJavalinImplementation //
+ /////////////////////////////////////////////////
Map authenticationContext = new HashMap<>();
String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY);
String authorizationHeaderValue = context.header("Authorization");
String apiKeyHeaderValue = context.header("x-api-key");
+ String codeQueryParamValue = context.queryParam("code");
+ String stateQueryParamValue = context.queryParam("state");
- if(StringUtils.hasContent(sessionIdCookieValue))
+ if(StringUtils.hasContent(codeQueryParamValue) && StringUtils.hasContent(stateQueryParamValue))
+ {
+ authenticationContext.put("code", codeQueryParamValue);
+ authenticationContext.put("state", stateQueryParamValue);
+ }
+ else if(StringUtils.hasContent(sessionIdCookieValue))
{
///////////////////////////////////////////////////////
// sessionId - maybe used by table-based auth module //
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java
new file mode 100644
index 00000000..82c09173
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java
@@ -0,0 +1,209 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.middleware.javalin.metadata;
+
+
+import java.util.List;
+import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class JavalinRouteProviderMetaData implements QMetaDataObject
+{
+ private String hostedPath;
+
+ private String fileSystemPath;
+ private String processName;
+
+ private List methods;
+
+ private QCodeReference routeAuthenticator;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public JavalinRouteProviderMetaData()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for hostedPath
+ *******************************************************************************/
+ public String getHostedPath()
+ {
+ return (this.hostedPath);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for hostedPath
+ *******************************************************************************/
+ public void setHostedPath(String hostedPath)
+ {
+ this.hostedPath = hostedPath;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for hostedPath
+ *******************************************************************************/
+ public JavalinRouteProviderMetaData withHostedPath(String hostedPath)
+ {
+ this.hostedPath = hostedPath;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for fileSystemPath
+ *******************************************************************************/
+ public String getFileSystemPath()
+ {
+ return (this.fileSystemPath);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for fileSystemPath
+ *******************************************************************************/
+ public void setFileSystemPath(String fileSystemPath)
+ {
+ this.fileSystemPath = fileSystemPath;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for fileSystemPath
+ *******************************************************************************/
+ public JavalinRouteProviderMetaData withFileSystemPath(String fileSystemPath)
+ {
+ this.fileSystemPath = fileSystemPath;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for processName
+ *******************************************************************************/
+ public String getProcessName()
+ {
+ return (this.processName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for processName
+ *******************************************************************************/
+ public void setProcessName(String processName)
+ {
+ this.processName = processName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for processName
+ *******************************************************************************/
+ public JavalinRouteProviderMetaData withProcessName(String processName)
+ {
+ this.processName = processName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for methods
+ *******************************************************************************/
+ public List getMethods()
+ {
+ return (this.methods);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for methods
+ *******************************************************************************/
+ public void setMethods(List methods)
+ {
+ this.methods = methods;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for methods
+ *******************************************************************************/
+ public JavalinRouteProviderMetaData withMethods(List methods)
+ {
+ this.methods = methods;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for routeAuthenticator
+ *******************************************************************************/
+ public QCodeReference getRouteAuthenticator()
+ {
+ return (this.routeAuthenticator);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for routeAuthenticator
+ *******************************************************************************/
+ public void setRouteAuthenticator(QCodeReference routeAuthenticator)
+ {
+ this.routeAuthenticator = routeAuthenticator;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for routeAuthenticator
+ *******************************************************************************/
+ public JavalinRouteProviderMetaData withRouteAuthenticator(QCodeReference routeAuthenticator)
+ {
+ this.routeAuthenticator = routeAuthenticator;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/misc/DownloadFileSupplementalAction.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/misc/DownloadFileSupplementalAction.java
index 3ab31747..2fd39d5c 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/misc/DownloadFileSupplementalAction.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/misc/DownloadFileSupplementalAction.java
@@ -193,6 +193,16 @@ public interface DownloadFileSupplementalAction
***************************************************************************/
class DownloadFileSupplementalActionOutput
{
-
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public DownloadFileSupplementalActionOutput()
+ {
+ ////////////////////////////////////////////////////////////////
+ // sorry, but here just to get test-coverage on this class... //
+ ////////////////////////////////////////////////////////////////
+ int i = 0;
+ }
}
}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java
new file mode 100644
index 00000000..02dad90b
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java
@@ -0,0 +1,303 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.middleware.javalin.routeproviders;
+
+
+import java.io.InputStream;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
+import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
+import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
+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.session.QSystemUserSession;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
+import com.kingsrook.qqq.backend.javalin.QJavalinUtils;
+import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
+import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
+import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface;
+import io.javalin.apibuilder.ApiBuilder;
+import io.javalin.apibuilder.EndpointGroup;
+import io.javalin.http.Context;
+import io.javalin.http.HttpStatus;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ProcessBasedRouter implements QJavalinRouteProviderInterface
+{
+ private static final QLogger LOG = QLogger.getLogger(ProcessBasedRouter.class);
+
+ private final String hostedPath;
+ private final String processName;
+ private final List methods;
+
+ private QCodeReference routeAuthenticator;
+
+ private QInstance qInstance;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ProcessBasedRouter(String hostedPath, String processName)
+ {
+ this(hostedPath, processName, null);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public ProcessBasedRouter(JavalinRouteProviderMetaData routeProvider)
+ {
+ this(routeProvider.getHostedPath(), routeProvider.getProcessName(), routeProvider.getMethods());
+ setRouteAuthenticator(routeProvider.getRouteAuthenticator());
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ProcessBasedRouter(String hostedPath, String processName, List methods)
+ {
+ this.hostedPath = hostedPath;
+ this.processName = processName;
+
+ if(CollectionUtils.nullSafeHasContents(methods))
+ {
+ this.methods = methods;
+ }
+ else
+ {
+ this.methods = List.of("GET");
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setQInstance(QInstance qInstance)
+ {
+ this.qInstance = qInstance;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public EndpointGroup getJavalinEndpointGroup()
+ {
+ return (() ->
+ {
+ for(String method : methods)
+ {
+ switch(method.toLowerCase())
+ {
+ case "get" -> ApiBuilder.get(hostedPath, this::handleRequest);
+ case "post" -> ApiBuilder.post(hostedPath, this::handleRequest);
+ case "put" -> ApiBuilder.put(hostedPath, this::handleRequest);
+ case "patch" -> ApiBuilder.patch(hostedPath, this::handleRequest);
+ case "delete" -> ApiBuilder.delete(hostedPath, this::handleRequest);
+ default -> throw (new IllegalArgumentException("Unrecognized method: " + method));
+ }
+ }
+ });
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void handleRequest(Context context)
+ {
+ RunProcessInput input = new RunProcessInput();
+ input.setProcessName(processName);
+
+ QContext.init(qInstance, new QSystemUserSession());
+
+ boolean isAuthenticated = false;
+ if(routeAuthenticator == null)
+ {
+ isAuthenticated = true;
+ }
+ else
+ {
+ try
+ {
+ RouteAuthenticatorInterface routeAuthenticator = QCodeLoader.getAdHoc(RouteAuthenticatorInterface.class, this.routeAuthenticator);
+ isAuthenticated = routeAuthenticator.authenticateRequest(context);
+ }
+ catch(Exception e)
+ {
+ context.skipRemainingHandlers();
+ QJavalinImplementation.handleException(context, e);
+ }
+ }
+
+ if(!isAuthenticated)
+ {
+ LOG.info("Request is not authenticated, so returning before running process", logPair("processName", processName), logPair("path", context.path()));
+ return;
+ }
+
+ try
+ {
+ LOG.info("Running process to serve route", logPair("processName", processName), logPair("path", context.path()));
+
+ /////////////////////
+ // run the process //
+ /////////////////////
+ input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
+ input.addValue("path", context.path());
+ input.addValue("method", context.method());
+ input.addValue("pathParams", new HashMap<>(context.pathParamMap()));
+ input.addValue("queryParams", new HashMap<>(context.queryParamMap()));
+ input.addValue("formParams", new HashMap<>(context.formParamMap()));
+ input.addValue("cookies", new HashMap<>(context.cookieMap()));
+ input.addValue("requestHeaders", new HashMap<>(context.headerMap()));
+
+ RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
+
+ /////////////////
+ // headers map //
+ /////////////////
+ Serializable headers = runProcessOutput.getValue("responseHeaders");
+ if(headers instanceof Map headersMap)
+ {
+ for(Object key : headersMap.keySet())
+ {
+ context.header(ValueUtils.getValueAsString(key), ValueUtils.getValueAsString(headersMap.get(key)));
+ }
+ }
+
+ // todo - make the inputStream available to the process
+ // maybe via the callback object??? input.setCallback(new QProcessCallback() {});
+ // context.resultInputStream();
+
+ //////////////
+ // response //
+ //////////////
+ Integer statusCode = runProcessOutput.getValueInteger("statusCode");
+ String redirectURL = runProcessOutput.getValueString("redirectURL");
+ String responseString = runProcessOutput.getValueString("responseString");
+ byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes");
+ StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput");
+
+ if(StringUtils.hasContent(redirectURL))
+ {
+ context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode));
+ return;
+ }
+
+ if(statusCode != null)
+ {
+ context.status(statusCode);
+ }
+
+ if(StringUtils.hasContent(responseString))
+ {
+ context.result(responseString);
+ return;
+ }
+
+ if(responseBytes != null && responseBytes.length > 0)
+ {
+ context.result(responseBytes);
+ return;
+ }
+
+ if(responseStorageInput != null)
+ {
+ InputStream inputStream = new StorageAction().getInputStream(responseStorageInput);
+ context.result(inputStream);
+ return;
+ }
+
+ LOG.debug("No response value was set in the process output state.");
+ }
+ catch(Exception e)
+ {
+ QJavalinUtils.handleException(null, context, e);
+ }
+ finally
+ {
+ QContext.clear();
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for routeAuthenticator
+ *******************************************************************************/
+ public QCodeReference getRouteAuthenticator()
+ {
+ return (this.routeAuthenticator);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for routeAuthenticator
+ *******************************************************************************/
+ public void setRouteAuthenticator(QCodeReference routeAuthenticator)
+ {
+ this.routeAuthenticator = routeAuthenticator;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for routeAuthenticator
+ *******************************************************************************/
+ public ProcessBasedRouter withRouteAuthenticator(QCodeReference routeAuthenticator)
+ {
+ this.routeAuthenticator = routeAuthenticator;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java
new file mode 100644
index 00000000..8d754469
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java
@@ -0,0 +1,413 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.middleware.javalin.routeproviders;
+
+
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
+import com.kingsrook.qqq.backend.core.model.actions.processes.QProcessPayload;
+
+
+/*******************************************************************************
+ ** process payload shared the processes which are used as process-based-router
+ ** processes. e.g., the fields here are those written to and read by
+ ** ProcessBasedRouter.
+ *******************************************************************************/
+public class ProcessBasedRouterPayload extends QProcessPayload
+{
+ private String path;
+ private String method;
+ private Map pathParams;
+ private Map> queryParams;
+ private Map> formParams;
+ private Map cookies;
+
+ private Integer statusCode;
+ private String redirectURL;
+ private Map responseHeaders;
+ private String responseString;
+ private byte[] responseBytes;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ProcessBasedRouterPayload()
+ {
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ProcessBasedRouterPayload(ProcessState processState)
+ {
+ this.populateFromProcessState(processState);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for path
+ *******************************************************************************/
+ public String getPath()
+ {
+ return (this.path);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for path
+ *******************************************************************************/
+ public void setPath(String path)
+ {
+ this.path = path;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for path
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withPath(String path)
+ {
+ this.path = path;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for method
+ *******************************************************************************/
+ public String getMethod()
+ {
+ return (this.method);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for method
+ *******************************************************************************/
+ public void setMethod(String method)
+ {
+ this.method = method;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for method
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withMethod(String method)
+ {
+ this.method = method;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for pathParams
+ *******************************************************************************/
+ public Map getPathParams()
+ {
+ return (this.pathParams);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for pathParams
+ *******************************************************************************/
+ public void setPathParams(Map pathParams)
+ {
+ this.pathParams = pathParams;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for pathParams
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withPathParams(Map pathParams)
+ {
+ this.pathParams = pathParams;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for cookies
+ *******************************************************************************/
+ public Map getCookies()
+ {
+ return (this.cookies);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for cookies
+ *******************************************************************************/
+ public void setCookies(Map cookies)
+ {
+ this.cookies = cookies;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for cookies
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withCookies(Map cookies)
+ {
+ this.cookies = cookies;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for statusCode
+ *******************************************************************************/
+ public Integer getStatusCode()
+ {
+ return (this.statusCode);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for statusCode
+ *******************************************************************************/
+ public void setStatusCode(Integer statusCode)
+ {
+ this.statusCode = statusCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for statusCode
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withStatusCode(Integer statusCode)
+ {
+ this.statusCode = statusCode;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for responseHeaders
+ *******************************************************************************/
+ public Map getResponseHeaders()
+ {
+ return (this.responseHeaders);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for responseHeaders
+ *******************************************************************************/
+ public void setResponseHeaders(Map responseHeaders)
+ {
+ this.responseHeaders = responseHeaders;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for responseHeaders
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withResponseHeaders(Map responseHeaders)
+ {
+ this.responseHeaders = responseHeaders;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for responseString
+ *******************************************************************************/
+ public String getResponseString()
+ {
+ return (this.responseString);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for responseString
+ *******************************************************************************/
+ public void setResponseString(String responseString)
+ {
+ this.responseString = responseString;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for responseString
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withResponseString(String responseString)
+ {
+ this.responseString = responseString;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for responseBytes
+ *******************************************************************************/
+ public byte[] getResponseBytes()
+ {
+ return (this.responseBytes);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for responseBytes
+ *******************************************************************************/
+ public void setResponseBytes(byte[] responseBytes)
+ {
+ this.responseBytes = responseBytes;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for responseBytes
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withResponseBytes(byte[] responseBytes)
+ {
+ this.responseBytes = responseBytes;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for redirectURL
+ *******************************************************************************/
+ public String getRedirectURL()
+ {
+ return (this.redirectURL);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for redirectURL
+ *******************************************************************************/
+ public void setRedirectURL(String redirectURL)
+ {
+ this.redirectURL = redirectURL;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for redirectURL
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withRedirectURL(String redirectURL)
+ {
+ this.redirectURL = redirectURL;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for queryParams
+ *******************************************************************************/
+ public Map> getQueryParams()
+ {
+ return (this.queryParams);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for queryParams
+ *******************************************************************************/
+ public void setQueryParams(Map> queryParams)
+ {
+ this.queryParams = queryParams;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for queryParams
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withQueryParams(Map> queryParams)
+ {
+ this.queryParams = queryParams;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for formParams
+ *******************************************************************************/
+ public Map> getFormParams()
+ {
+ return (this.formParams);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for formParams
+ *******************************************************************************/
+ public void setFormParams(Map> formParams)
+ {
+ this.formParams = formParams;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for formParams
+ *******************************************************************************/
+ public ProcessBasedRouterPayload withFormParams(Map> formParams)
+ {
+ this.formParams = formParams;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java
new file mode 100644
index 00000000..c6504be1
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java
@@ -0,0 +1,224 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.middleware.javalin.routeproviders;
+
+
+import java.net.URL;
+import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+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.session.QSystemUserSession;
+import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
+import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
+import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
+import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface;
+import io.javalin.Javalin;
+import io.javalin.config.JavalinConfig;
+import io.javalin.http.Context;
+import io.javalin.http.staticfiles.Location;
+import io.javalin.http.staticfiles.StaticFileConfig;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** javalin route provider that hosts a path in the http server via a path on
+ ** the file system
+ *******************************************************************************/
+public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInterface
+{
+ private static final QLogger LOG = QLogger.getLogger(SimpleFileSystemDirectoryRouter.class);
+
+ private final String hostedPath;
+ private final String fileSystemPath;
+
+ private QCodeReference routeAuthenticator;
+
+ private QInstance qInstance;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public SimpleFileSystemDirectoryRouter(String hostedPath, String fileSystemPath)
+ {
+ this.hostedPath = hostedPath;
+ this.fileSystemPath = fileSystemPath;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public SimpleFileSystemDirectoryRouter(JavalinRouteProviderMetaData routeProvider)
+ {
+ this(routeProvider.getHostedPath(), routeProvider.getFileSystemPath());
+ setRouteAuthenticator(routeProvider.getRouteAuthenticator());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void setQInstance(QInstance qInstance)
+ {
+ this.qInstance = qInstance;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void handleJavalinStaticFileConfig(StaticFileConfig staticFileConfig)
+ {
+ URL resource = getClass().getClassLoader().getResource(fileSystemPath);
+ if(resource == null)
+ {
+ String message = "Could not find file system path: " + fileSystemPath;
+ if(fileSystemPath.startsWith("/") && getClass().getClassLoader().getResource(fileSystemPath.replaceFirst("^/+", "")) != null)
+ {
+ message += ". For non-absolute paths, do not prefix with a leading slash.";
+ }
+ throw new RuntimeException(message);
+ }
+
+ if(!hostedPath.startsWith("/"))
+ {
+ LOG.warn("hostedPath [" + hostedPath + "] should probably start with a leading slash...");
+ }
+
+ staticFileConfig.directory = resource.getFile();
+ staticFileConfig.hostedPath = hostedPath;
+ staticFileConfig.location = Location.EXTERNAL;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void before(Context context) throws QException
+ {
+ LOG.debug("In before handler for simpleFileSystemRouter", logPair("hostedPath", hostedPath));
+ QContext.init(qInstance, new QSystemUserSession());
+
+ if(routeAuthenticator != null)
+ {
+ try
+ {
+ RouteAuthenticatorInterface routeAuthenticator = QCodeLoader.getAdHoc(RouteAuthenticatorInterface.class, this.routeAuthenticator);
+ boolean isAuthenticated = routeAuthenticator.authenticateRequest(context);
+ if(!isAuthenticated)
+ {
+ LOG.info("Static file request is not authenticated, so telling javalin to skip remaining handlers", logPair("path", context.path()));
+ context.skipRemainingHandlers();
+ }
+ }
+ catch(Exception e)
+ {
+ context.skipRemainingHandlers();
+ QJavalinImplementation.handleException(context, e);
+ }
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private void after(Context context)
+ {
+ LOG.debug("In after handler for simpleFileSystemRouter", logPair("hostedPath", hostedPath));
+ QContext.clear();
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void acceptJavalinConfig(JavalinConfig config)
+ {
+ config.staticFiles.add(this::handleJavalinStaticFileConfig);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void acceptJavalinService(Javalin service)
+ {
+ String javalinPath = hostedPath;
+ if(!javalinPath.endsWith("/"))
+ {
+ javalinPath += "/";
+ }
+ javalinPath += "";
+
+ service.before(javalinPath, this::before);
+ service.before(javalinPath, this::after);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for routeAuthenticator
+ *******************************************************************************/
+ public QCodeReference getRouteAuthenticator()
+ {
+ return (this.routeAuthenticator);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for routeAuthenticator
+ *******************************************************************************/
+ public void setRouteAuthenticator(QCodeReference routeAuthenticator)
+ {
+ this.routeAuthenticator = routeAuthenticator;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for routeAuthenticator
+ *******************************************************************************/
+ public SimpleFileSystemDirectoryRouter withRouteAuthenticator(QCodeReference routeAuthenticator)
+ {
+ this.routeAuthenticator = routeAuthenticator;
+ return (this);
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/RouteAuthenticatorInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/RouteAuthenticatorInterface.java
new file mode 100644
index 00000000..c08b09ef
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/RouteAuthenticatorInterface.java
@@ -0,0 +1,43 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.middleware.javalin.routeproviders.authentication;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import io.javalin.http.Context;
+
+
+/*******************************************************************************
+ ** interface used by QJavalinRouteProviderInterface subclasses, to interact with
+ ** QQQ Authentication modules, to provide authentication to custom javalin routes.
+ *******************************************************************************/
+public interface RouteAuthenticatorInterface
+{
+
+ /***************************************************************************
+ ** where authentication for a route occurs, before the route is served.
+ **
+ ** @return true if request is authenticated; else false
+ ***************************************************************************/
+ boolean authenticateRequest(Context context) throws QException;
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java
new file mode 100644
index 00000000..82f83024
--- /dev/null
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java
@@ -0,0 +1,94 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.middleware.javalin.routeproviders.authentication;
+
+
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
+import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
+import io.javalin.http.Context;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** simple implementation of a route authenticator. Assumes that unauthenticated
+ ** requests should redirect to a login page. Note though, maybe that should be
+ ** more intelligent, like, only redirect requests for a .html file, but not
+ ** requests for include files like images or .js/.css?
+ *******************************************************************************/
+public class SimpleRouteAuthenticator implements RouteAuthenticatorInterface
+{
+ private static final QLogger LOG = QLogger.getLogger(SimpleRouteAuthenticator.class);
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public boolean authenticateRequest(Context context) throws QException
+ {
+ try
+ {
+ QSession qSession = QJavalinImplementation.setupSession(context, null);
+ LOG.debug("Session has been activated", logPair("uuid", qSession.getUuid()));
+
+ if(context.queryParamMap().containsKey("code") && context.queryParamMap().containsKey("state"))
+ {
+ //////////////////////////////////////////////////////////////////////////
+ // if this request was a callback from oauth, with code & state params, //
+ // then redirect one last time removing those from the query string //
+ //////////////////////////////////////////////////////////////////////////
+ String redirectURL = context.fullUrl().replace("code=" + context.queryParam("code"), "")
+ .replace("state=" + context.queryParam("state"), "")
+ .replaceFirst("&+$", "")
+ .replaceFirst("\\?&", "?")
+ .replaceFirst("\\?$", "");
+ context.redirect(redirectURL);
+ LOG.debug("Redirecting request to remove code and state parameters");
+ return (false);
+ }
+
+ return (true);
+ }
+ catch(QAuthenticationException e)
+ {
+ QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
+ QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(QContext.getQInstance().getAuthentication());
+
+ String redirectURL = authenticationModule.getLoginRedirectUrl(context.fullUrl());
+
+ context.redirect(redirectURL);
+ LOG.debug("Redirecting request, due to required session missing");
+ return (false);
+ }
+ catch(QModuleDispatchException e)
+ {
+ throw (new QException("Error authenticating request", e));
+ }
+ }
+
+}
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java
index cfb4c22e..5c625b1e 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java
@@ -242,23 +242,29 @@ public class MetaDataSpecV1 extends AbstractEndpointSpec
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ // double-wrap the context here, so the instance will exist when the system-user-session is created //
+ // to avoid warnings out of system-user-session about there not being an instance in context. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////
+ QContext.withTemporaryContext(new CapturedContext(exampleInstance, null), () ->
{
- try
+ QContext.withTemporaryContext(new CapturedContext(exampleInstance, new QSystemUserSession()), () ->
{
- MetaDataAction metaDataAction = new MetaDataAction();
- MetaDataOutput output = metaDataAction.execute(new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput());
- examples.put("Example", new Example()
- .withValue(new MetaDataResponseV1()
- .withMetaDataOutput(output)
- )
- );
- }
- catch(Exception e)
- {
- examples.put("Example", new Example().withValue("Error building example: " + e.getMessage())
- );
- }
+ try
+ {
+ MetaDataAction metaDataAction = new MetaDataAction();
+ MetaDataOutput output = metaDataAction.execute(new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput());
+ examples.put("Example", new Example()
+ .withValue(new MetaDataResponseV1()
+ .withMetaDataOutput(output)
+ )
+ );
+ }
+ catch(Exception e)
+ {
+ examples.put("Example", new Example().withValue("Error building example: " + e.getMessage()));
+ }
+ });
});
return new BasicResponse("""
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
index 6773f4fe..59ae8177 100644
--- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -28,6 +28,7 @@ import java.sql.Connection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
@@ -95,6 +96,9 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
+import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
+import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouterPayload;
+import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.SimpleRouteAuthenticator;
import org.apache.commons.io.IOUtils;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -123,18 +127,17 @@ public class TestUtils
public static final String SCREEN_0 = "screen0";
public static final String SCREEN_1 = "screen1";
+ public static final String STATIC_SITE_PATH = "static-site";
+
/*******************************************************************************
** Prime a test database (e.g., h2, in-memory)
**
*******************************************************************************/
- @SuppressWarnings("unchecked")
public static void primeTestDatabase() throws Exception
{
- ConnectionManager connectionManager = new ConnectionManager();
-
- try(Connection connection = connectionManager.getConnection(TestUtils.defineDefaultH2Backend()))
+ try(Connection connection = ConnectionManager.getConnection(TestUtils.defineDefaultH2Backend()))
{
InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql");
assertNotNull(primeTestDatabaseSqlStream);
@@ -156,8 +159,7 @@ public class TestUtils
*******************************************************************************/
public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception
{
- ConnectionManager connectionManager = new ConnectionManager();
- try(Connection connection = connectionManager.getConnection(defineDefaultH2Backend()))
+ try(Connection connection = ConnectionManager.getConnection(defineDefaultH2Backend()))
{
QueryManager.executeStatement(connection, sql, resultSetProcessor);
}
@@ -184,10 +186,33 @@ public class TestUtils
qInstance.addProcess(defineProcessScreenThenSleep());
qInstance.addProcess(defineProcessPutsNullKeyInMap());
qInstance.addProcess(defineProcessSimpleThrow());
+ qInstance.addProcess(defineRouterProcess());
qInstance.addReport(definePersonsReport());
qInstance.addPossibleValueSource(definePossibleValueSourcePerson());
defineWidgets(qInstance);
+ List routeProviders = new ArrayList<>();
+ routeProviders.add(new JavalinRouteProviderMetaData()
+ .withHostedPath("/statically-served")
+ .withFileSystemPath(STATIC_SITE_PATH));
+
+ routeProviders.add(new JavalinRouteProviderMetaData()
+ .withHostedPath("/protected-statically-served")
+ .withFileSystemPath(STATIC_SITE_PATH)
+ .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class)));
+
+ routeProviders.add(new JavalinRouteProviderMetaData()
+ .withHostedPath("/served-by-process/")
+ .withMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE"))
+ .withProcessName("routerProcess"));
+
+ routeProviders.add(new JavalinRouteProviderMetaData()
+ .withHostedPath("/protected-served-by-process/")
+ .withProcessName("routerProcess")
+ .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class)));
+
+ qInstance.withSupplementalMetaData(new QJavalinMetaData().withRouteProviders(routeProviders));
+
qInstance.addBackend(defineMemoryBackend());
try
{
@@ -206,6 +231,55 @@ public class TestUtils
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ private static QProcessMetaData defineRouterProcess()
+ {
+ return (new QProcessMetaData()
+ .withName("routerProcess")
+ .withStep(new QBackendStepMetaData()
+ .withName("step")
+ .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) ->
+ {
+ ProcessBasedRouterPayload processPayload = runBackendStepInput.getProcessPayload(ProcessBasedRouterPayload.class);
+ String path = processPayload.getPath();
+
+ if(processPayload.getQueryParams().containsKey("requestedRedirect"))
+ {
+ processPayload.setRedirectURL(processPayload.getQueryParams().get("requestedRedirect").get(0));
+ }
+ else
+ {
+ String response = "So you've done a " + processPayload.getMethod() + " for: " + path;
+ if(processPayload.getQueryParams().containsKey("respondInBytes"))
+ {
+ processPayload.setResponseBytes(response.getBytes(StandardCharsets.UTF_8));
+ }
+ else if(processPayload.getQueryParams().containsKey("noResponse"))
+ {
+ ///////////////////////////////////////
+ // don't call any setResponse method //
+ ///////////////////////////////////////
+ }
+ else if(processPayload.getQueryParams().containsKey("doThrow"))
+ {
+ throw (new QException("Test Exception"));
+ }
+ else
+ {
+ processPayload.setResponseString(response);
+ }
+ processPayload.setResponseHeaders(Map.of("X-Test", "Yes, Test"));
+ processPayload.setStatusCode(200);
+ }
+ runBackendStepOutput.setProcessPayload(processPayload);
+ }))
+ ));
+ }
+
+
+
/***************************************************************************
**
***************************************************************************/
@@ -567,7 +641,6 @@ public class TestUtils
-
/*******************************************************************************
** Define an interactive version of the 'greet people' process
*******************************************************************************/
@@ -587,6 +660,7 @@ public class TestUtils
}
+
/*******************************************************************************
** Define a process with just one step that sleeps and then throws
*******************************************************************************/
@@ -754,7 +828,7 @@ public class TestUtils
{
return (new RenderWidgetOutput(new RawHTML("title",
QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE_OFFSET_MINUTES)
- + "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE)
+ + "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE)
)));
}
}
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java
index a1cdf6b2..a6bf4085 100644
--- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java
@@ -22,18 +22,22 @@
package com.kingsrook.qqq.middleware.javalin;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.javalin.TestUtils;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
+import io.javalin.http.HttpStatus;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.json.JSONObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -52,7 +56,7 @@ class QApplicationJavalinServerTest
**
*******************************************************************************/
@AfterEach
- void afterEach()
+ void afterEach() throws IOException
{
javalinServer.stop();
TestApplication.callCount = 0;
@@ -123,6 +127,7 @@ class QApplicationJavalinServerTest
}
+
/*******************************************************************************
**
*******************************************************************************/
@@ -173,6 +178,124 @@ class QApplicationJavalinServerTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testStaticRouter() throws Exception
+ {
+ javalinServer = new QApplicationJavalinServer(getQqqApplication())
+ .withServeFrontendMaterialDashboard(false)
+ .withPort(PORT);
+ javalinServer.start();
+
+ Unirest.config().setDefaultResponseEncoding("UTF-8");
+ HttpResponse response = Unirest.get("http://localhost:" + PORT + "/statically-served/foo.html").asString();
+ assertEquals("Foo? Bar!", response.getBody());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthenticatedStaticRouter() throws Exception
+ {
+ javalinServer = new QApplicationJavalinServer(getQqqApplication())
+ .withServeFrontendMaterialDashboard(false)
+ .withPort(PORT);
+ javalinServer.start();
+
+ Unirest.config().setDefaultResponseEncoding("UTF-8")
+ .reset()
+ .followRedirects(false);
+
+ HttpResponse response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html")
+ .header("Authorization", "Bearer Deny")
+ .asString();
+
+ assertEquals(HttpStatus.FOUND.getCode(), response.getStatus());
+ assertThat(response.getHeaders().getFirst("Location")).contains("createMockSession");
+
+ response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html")
+ .asString();
+ assertEquals(HttpStatus.OK.getCode(), response.getStatus());
+ assertEquals("Foo? Bar!", response.getBody());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testProcessRouter() throws Exception
+ {
+ javalinServer = new QApplicationJavalinServer(getQqqApplication())
+ .withServeFrontendMaterialDashboard(false)
+ .withPort(PORT);
+ javalinServer.start();
+
+ HttpResponse response = Unirest.get("http://localhost:" + PORT + "/served-by-process/foo.html").asString();
+ assertEquals(200, response.getStatus());
+ assertEquals("So you've done a GET for: /served-by-process/foo.html", response.getBody());
+
+ response = Unirest.post("http://localhost:" + PORT + "/served-by-process/foo.html").asString();
+ assertEquals(200, response.getStatus());
+ assertEquals("So you've done a POST for: /served-by-process/foo.html", response.getBody());
+ assertEquals("Yes, Test", response.getHeaders().getFirst("X-Test"));
+
+ response = Unirest.put("http://localhost:" + PORT + "/served-by-process/foo.html?requestedRedirect=google.com").asString();
+ assertEquals(302, response.getStatus());
+ assertEquals("google.com", response.getHeaders().getFirst("Location"));
+
+ HttpResponse responseBytes = Unirest.delete("http://localhost:" + PORT + "/served-by-process/foo.html?respondInBytes=true").asBytes();
+ assertEquals(200, responseBytes.getStatus());
+ assertArrayEquals("So you've done a DELETE for: /served-by-process/foo.html".getBytes(StandardCharsets.UTF_8), responseBytes.getBody());
+
+ response = Unirest.get("http://localhost:" + PORT + "/served-by-process/foo.html?noResponse=true").asString();
+ assertEquals(200, response.getStatus());
+ assertEquals("", response.getBody());
+
+ response = Unirest.get("http://localhost:" + PORT + "/served-by-process/foo.html?doThrow=true").asString();
+ assertEquals(500, response.getStatus());
+ assertThat(response.getBody()).contains("Test Exception");
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthenticatedProcessRouter() throws Exception
+ {
+ javalinServer = new QApplicationJavalinServer(getQqqApplication())
+ .withServeFrontendMaterialDashboard(false)
+ .withPort(PORT);
+ javalinServer.start();
+
+ Unirest.config().setDefaultResponseEncoding("UTF-8")
+ .reset()
+ .followRedirects(false);
+
+ HttpResponse response = Unirest.get("http://localhost:" + PORT + "/protected-served-by-process/foo.html")
+ .header("Authorization", "Bearer Deny")
+ .asString();
+
+ assertEquals(HttpStatus.FOUND.getCode(), response.getStatus());
+ assertThat(response.getHeaders().getFirst("Location")).contains("createMockSession");
+
+ response = Unirest.get("http://localhost:" + PORT + "/protected-served-by-process/foo.html")
+ .asString();
+ assertEquals(200, response.getStatus());
+ assertEquals("So you've done a GET for: /protected-served-by-process/foo.html", response.getBody());
+ }
+
+
+
/***************************************************************************
**
***************************************************************************/
diff --git a/qqq-middleware-javalin/src/test/resources/static-site/foo.html b/qqq-middleware-javalin/src/test/resources/static-site/foo.html
new file mode 100644
index 00000000..22b50adf
--- /dev/null
+++ b/qqq-middleware-javalin/src/test/resources/static-site/foo.html
@@ -0,0 +1 @@
+Foo? Bar!
\ No newline at end of file
diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/ConfigFileBasedSampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/ConfigFileBasedSampleJavalinServer.java
new file mode 100644
index 00000000..07973c3a
--- /dev/null
+++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/ConfigFileBasedSampleJavalinServer.java
@@ -0,0 +1,82 @@
+/*
+ * 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.sampleapp;
+
+
+import java.util.Arrays;
+import com.kingsrook.qqq.backend.core.instances.ConfigFilesBasedQQQApplication;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
+import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class ConfigFileBasedSampleJavalinServer
+{
+ private static final QLogger LOG = QLogger.getLogger(ConfigFileBasedSampleJavalinServer.class);
+ private final String path;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void main(String[] args)
+ {
+ String path = "src/main/resources/metadata";
+ if(args.length > 0)
+ {
+ path = args[0];
+ System.out.println("Using path from args [" + path + "]");
+ }
+
+ new ConfigFileBasedSampleJavalinServer(path).start();
+ }
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public ConfigFileBasedSampleJavalinServer(String path)
+ {
+ this.path = path;
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void start()
+ {
+ try
+ {
+ new QApplicationJavalinServer(new ConfigFilesBasedQQQApplication(path)).start();
+ }
+ catch(Exception e)
+ {
+ LOG.error("Failed to start javalin server. See stack trace for details.", e);
+ }
+ }
+
+}
diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java
index cd828001..fd1b468b 100644
--- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java
+++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java
@@ -50,7 +50,25 @@ public class SampleCli
{
try
{
- QInstance qInstance = SampleMetaDataProvider.defineInstance();
+ QInstance qInstance = SampleMetaDataProvider.defineInstance();
+ return (run(qInstance, args));
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ return (-1);
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ int run(QInstance qInstance, String[] args)
+ {
+ try
+ {
QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance);
return (qPicoCliImplementation.runCli("my-sample-cli", args));
diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java
index 4ab4f375..83753e55 100644
--- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java
+++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java
@@ -25,6 +25,7 @@ package com.kingsrook.sampleapp;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider;
+import static com.kingsrook.sampleapp.metadata.SampleMetaDataProvider.primeTestDatabase;
/*******************************************************************************
@@ -53,7 +54,11 @@ public class SampleJavalinServer
{
try
{
- new QApplicationJavalinServer(new SampleMetaDataProvider()).start();
+ primeTestDatabase("prime-test-database.sql");
+
+ QApplicationJavalinServer javalinServer = new QApplicationJavalinServer(new SampleMetaDataProvider());
+
+ javalinServer.start();
}
catch(Exception e)
{
diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/DynamicSiteProcessMetaDataProducer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/DynamicSiteProcessMetaDataProducer.java
new file mode 100644
index 00000000..92d65133
--- /dev/null
+++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/DynamicSiteProcessMetaDataProducer.java
@@ -0,0 +1,56 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.sampleapp.metadata;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
+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.processes.QBackendStepMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
+import com.kingsrook.sampleapp.processes.dynamicsite.DynamicSiteProcessStep;
+
+
+/*******************************************************************************
+ ** Meta Data Producer for DynamicSiteProcess
+ *******************************************************************************/
+public class DynamicSiteProcessMetaDataProducer extends MetaDataProducer
+{
+ public static final String NAME = "DynamicSiteProcess";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QProcessMetaData produce(QInstance qInstance) throws QException
+ {
+ return (new QProcessMetaData()
+ .withName(NAME)
+ .withStep(new QBackendStepMetaData()
+ .withName("DynamicSiteProcessStep")
+ .withCode(new QCodeReference(DynamicSiteProcessStep.class))));
+ }
+
+}
diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java
new file mode 100644
index 00000000..49a30db3
--- /dev/null
+++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java
@@ -0,0 +1,67 @@
+/*
+ * 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.sampleapp.metadata;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.OAuth2AuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.RedirectStateMetaDataProducer;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession;
+
+
+/*******************************************************************************
+ ** Provides all OAuth2 authentication related metadata to the QQQ engine
+ *
+ *******************************************************************************/
+public class OAuth2MetaDataProvider implements MetaDataProducerInterface
+{
+ public static final String NAME = "OAuth2";
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public QAuthenticationMetaData produce(QInstance qInstance) throws QException
+ {
+ QMetaDataVariableInterpreter qMetaDataVariableInterpreter = new QMetaDataVariableInterpreter();
+
+ String oauth2BaseUrl = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_BASE_URL}");
+ String oauth2ClientId = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_CLIENT_ID}");
+ String oauth2ClientSecret = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_CLIENT_SECRET}");
+ String oauth2Scopes = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_SCOPES}");
+
+ return (new OAuth2AuthenticationMetaData()
+ .withBaseUrl(oauth2BaseUrl)
+ .withClientId(oauth2ClientId)
+ .withClientSecret(oauth2ClientSecret)
+ .withScopes(oauth2Scopes)
+ .withUserSessionTableName(UserSession.TABLE_NAME)
+ .withRedirectStateTableName(RedirectStateMetaDataProducer.TABLE_NAME)
+ .withName(NAME));
+ }
+}
diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleJavalinMetaDataProducer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleJavalinMetaDataProducer.java
new file mode 100644
index 00000000..602daec1
--- /dev/null
+++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleJavalinMetaDataProducer.java
@@ -0,0 +1,64 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.sampleapp.metadata;
+
+
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
+import com.kingsrook.qqq.backend.javalin.QJavalinMetaData;
+import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
+import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.SimpleRouteAuthenticator;
+
+
+/*******************************************************************************
+ ** Meta Data Producer for SampleJavalin
+ *******************************************************************************/
+public class SampleJavalinMetaDataProducer extends MetaDataProducer
+{
+
+ /*******************************************************************************
+ ** todo wip - test sub-directories of each other
+ ** todo wip - allow mat-dash to be served at a different path
+ ** todo wip - get mat-dash committed
+ *******************************************************************************/
+ @Override
+ public QJavalinMetaData produce(QInstance qInstance) throws QException
+ {
+ return (new QJavalinMetaData()
+ .withRouteProvider(new JavalinRouteProviderMetaData()
+ .withHostedPath("/public")
+ .withFileSystemPath("site/public"))
+
+ .withRouteProvider(new JavalinRouteProviderMetaData()
+ .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class))
+ .withHostedPath("/private")
+ .withFileSystemPath("site/private"))
+
+ .withRouteProvider(new JavalinRouteProviderMetaData()
+ .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class))
+ .withHostedPath("/dynamic-site/")
+ .withProcessName(DynamicSiteProcessMetaDataProducer.NAME)));
+ }
+
+}
diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java
index bc40d1a1..03df856c 100644
--- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java
+++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java
@@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+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.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
@@ -71,6 +72,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.RedirectStateMetaDataProducer;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.UserSessionMetaDataProducer;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
@@ -83,6 +87,7 @@ import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.Filesyst
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
+import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
import com.kingsrook.sampleapp.dashboard.widgets.PersonsByCreateDateBarChart;
import com.kingsrook.sampleapp.processes.clonepeople.ClonePeopleTransformStep;
import org.apache.commons.io.IOUtils;
@@ -97,6 +102,7 @@ public class SampleMetaDataProvider extends AbstractQQQApplication
public static final String RDBMS_BACKEND_NAME = "rdbms";
public static final String FILESYSTEM_BACKEND_NAME = "filesystem";
+ public static final String MEMORY_BACKEND_NAME = "memory";
public static final String APP_NAME_GREETINGS = "greetingsApp";
public static final String APP_NAME_PEOPLE = "peopleApp";
@@ -140,8 +146,8 @@ public class SampleMetaDataProvider extends AbstractQQQApplication
{
QInstance qInstance = new QInstance();
- qInstance.setAuthentication(defineAuthentication());
qInstance.addBackend(defineRdbmsBackend());
+ qInstance.addBackend(defineMemoryBackend());
qInstance.addBackend(defineFilesystemBackend());
qInstance.addTable(defineTableCarrier());
qInstance.addTable(defineTablePerson());
@@ -157,6 +163,9 @@ public class SampleMetaDataProvider extends AbstractQQQApplication
qInstance.addProcess(defineProcessScreenThenSleep());
qInstance.addProcess(defineProcessSimpleThrow());
+ qInstance.addTable(setTableBackendNamesForRdbms(new UserSessionMetaDataProducer(RDBMS_BACKEND_NAME).produce(qInstance)));
+ qInstance.addTable(setTableBackendNamesForRdbms(new RedirectStateMetaDataProducer(RDBMS_BACKEND_NAME).produce(qInstance)));
+
MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, SampleMetaDataProvider.class.getPackageName());
defineWidgets(qInstance);
@@ -168,6 +177,45 @@ public class SampleMetaDataProvider extends AbstractQQQApplication
+ /*******************************************************************************
+ ** if rdbms backend uses snake_case table & column names, instead of camelCase
+ ** style used for qqq meta-data tableNames and fieldNames, then set those via
+ ** this method.
+ *******************************************************************************/
+ private static QTableMetaData setTableBackendNamesForRdbms(QTableMetaData table)
+ {
+ table.setBackendDetails(new RDBMSTableBackendDetails()
+ .withTableName(QInstanceEnricher.inferBackendName(table.getName())));
+ QInstanceEnricher.setInferredFieldBackendNames(table);
+ return (table);
+ }
+
+
+
+ /***************************************************************************
+ ** for tests, define the same instance as above, but use mock authentication.
+ ***************************************************************************/
+ public static QInstance defineTestInstance() throws QException
+ {
+ QInstance qInstance = defineInstance();
+ qInstance.setAuthentication(defineAuthentication());
+ return qInstance;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private static QBackendMetaData defineMemoryBackend()
+ {
+ return new QBackendMetaData()
+ .withName(MEMORY_BACKEND_NAME)
+ .withBackendType(MemoryBackendModule.class);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/processes/dynamicsite/DynamicSiteProcessStep.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/processes/dynamicsite/DynamicSiteProcessStep.java
new file mode 100644
index 00000000..bef228e1
--- /dev/null
+++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/processes/dynamicsite/DynamicSiteProcessStep.java
@@ -0,0 +1,50 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2025. 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.sampleapp.processes.dynamicsite;
+
+
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouterPayload;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class DynamicSiteProcessStep implements BackendStep
+{
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ ProcessBasedRouterPayload processPayload = runBackendStepInput.getProcessPayload(ProcessBasedRouterPayload.class);
+
+ String path = processPayload.getPath();
+ processPayload.setResponseString("You requested: " + path + "(at path-param: " + processPayload.getPathParams().get("pagePath") + ")");
+ runBackendStepOutput.setProcessPayload(processPayload);
+ }
+
+}
diff --git a/qqq-sample-project/src/main/resources/metadata/filesystemBackend.yaml b/qqq-sample-project/src/main/resources/metadata/filesystemBackend.yaml
new file mode 100644
index 00000000..b190aab9
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/metadata/filesystemBackend.yaml
@@ -0,0 +1,5 @@
+---
+class: FilesystemBackendMetaData
+version: 1.0
+name: filesystem
+basePath: /tmp/sample-filesystem
\ No newline at end of file
diff --git a/qqq-sample-project/src/main/resources/metadata/javalin.yaml b/qqq-sample-project/src/main/resources/metadata/javalin.yaml
new file mode 100644
index 00000000..357e92bc
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/metadata/javalin.yaml
@@ -0,0 +1,4 @@
+---
+class: QJavalinMetaData
+version: 1.0
+loggerDisabled: true
\ No newline at end of file
diff --git a/qqq-sample-project/src/main/resources/metadata/mockAuthentication.yaml b/qqq-sample-project/src/main/resources/metadata/mockAuthentication.yaml
new file mode 100644
index 00000000..3188acdb
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/metadata/mockAuthentication.yaml
@@ -0,0 +1,5 @@
+---
+class: QAuthenticationMetaData
+version: 1.0
+name: mock
+type: MOCK
\ No newline at end of file
diff --git a/qqq-sample-project/src/main/resources/metadata/peopleApp.yaml b/qqq-sample-project/src/main/resources/metadata/peopleApp.yaml
new file mode 100644
index 00000000..d4af4deb
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/metadata/peopleApp.yaml
@@ -0,0 +1,10 @@
+---
+class: QAppMetaData
+version: 1.0
+name: people
+icon:
+ name: person
+sections:
+- name: People
+ tables:
+ - person
\ No newline at end of file
diff --git a/qqq-sample-project/src/main/resources/metadata/personTable.yaml b/qqq-sample-project/src/main/resources/metadata/personTable.yaml
new file mode 100644
index 00000000..1d8b0953
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/metadata/personTable.yaml
@@ -0,0 +1,41 @@
+---
+class: QTableMetaData
+version: 1.0
+name: person
+backendName: rdbms
+primaryKeyField: id
+recordLabelFormat: "%s %s"
+recordLabelFields:
+ - firstName
+ - lastName
+fields:
+ id:
+ type: INTEGER
+ isEditable: false
+ firstName:
+ type: STRING
+ lastName:
+ type: STRING
+## Field(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
+## Field(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false))
+## Field(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false))
+## Field(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name").withIsRequired(true))
+## Field(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name").withIsRequired(true))
+## Field(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date"))
+## Field(new QFieldMetaData("email", QFieldType.STRING))
+## Field(new QFieldMetaData("isEmployed", QFieldType.BOOLEAN).withBackendName("is_employed"))
+## Field(new QFieldMetaData("annualSalary", QFieldType.DECIMAL).withBackendName("annual_salary").withDisplayFormat(DisplayFormat.CURRENCY))
+## Field(new QFieldMetaData("daysWorked", QFieldType.INTEGER).withBackendName("days_worked").withDisplayFormat(DisplayFormat.COMMAS))
+sections:
+- name: identity
+ label: Identity
+ icon:
+ name: badge
+ tier: T1
+ fieldNames:
+ - id
+ - firstName
+ - lastName
+## Section(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("email", "birthDate")))
+## Section(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("isEmployed", "annualSalary", "daysWorked")))
+## Section(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
diff --git a/qqq-sample-project/src/main/resources/metadata/rdbmsBackend.yaml b/qqq-sample-project/src/main/resources/metadata/rdbmsBackend.yaml
new file mode 100644
index 00000000..58864338
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/metadata/rdbmsBackend.yaml
@@ -0,0 +1,10 @@
+---
+class: RDBMSBackendMetaData
+version: 1.0
+name: rdbms
+vendor: ${env.RDBMS_VENDOR}
+hostName: ${env.RDBMS_HOSTNAME}
+port: ${env.RDBMS_PORT}
+databaseName: ${env.RDBMS_DATABASE_NAME}
+username: ${env.RDBMS_USERNAME}
+password: ${env.RDBMS_PASSWORD}
diff --git a/qqq-sample-project/src/main/resources/prime-test-database.sql b/qqq-sample-project/src/main/resources/prime-test-database.sql
index 1f5e6bc9..5c0badc4 100644
--- a/qqq-sample-project/src/main/resources/prime-test-database.sql
+++ b/qqq-sample-project/src/main/resources/prime-test-database.sql
@@ -19,6 +19,31 @@
-- along with this program. If not, see .
--
+DROP TABLE IF EXISTS user_session;
+CREATE TABLE user_session
+(
+ id INTEGER AUTO_INCREMENT PRIMARY KEY,
+ create_date TIMESTAMP DEFAULT now(),
+ modify_date TIMESTAMP DEFAULT now(),
+ uuid VARCHAR(40) NOT NULL,
+ access_token MEDIUMTEXT,
+ user_id VARCHAR(100)
+);
+ALTER TABLE user_session ADD UNIQUE u_uuid (uuid);
+ALTER TABLE user_session ADD INDEX i_user_id (user_id);
+
+
+DROP TABLE IF EXISTS redirect_state;
+CREATE TABLE redirect_state
+(
+ id INTEGER AUTO_INCREMENT PRIMARY KEY,
+ create_date TIMESTAMP DEFAULT now(),
+ state VARCHAR(45) NOT NULL,
+ redirect_uri TEXT
+);
+ALTER TABLE redirect_state ADD UNIQUE u_state (state);
+
+
DROP TABLE IF EXISTS person;
CREATE TABLE person
(
diff --git a/qqq-sample-project/src/main/resources/site/private/index.html b/qqq-sample-project/src/main/resources/site/private/index.html
new file mode 100644
index 00000000..104100b8
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/site/private/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+Welcome to the private site
+
+
+
+
diff --git a/qqq-sample-project/src/main/resources/site/public/index.html b/qqq-sample-project/src/main/resources/site/public/index.html
new file mode 100644
index 00000000..768b2c21
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/site/public/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+Welcome to the public site
+
+
+
+
diff --git a/qqq-sample-project/src/main/resources/static-site/hello.txt b/qqq-sample-project/src/main/resources/static-site/hello.txt
new file mode 100644
index 00000000..e5b8f9ce
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/static-site/hello.txt
@@ -0,0 +1 @@
+World!
\ No newline at end of file
diff --git a/qqq-sample-project/src/main/resources/static-site/index.html b/qqq-sample-project/src/main/resources/static-site/index.html
new file mode 100644
index 00000000..611391a3
--- /dev/null
+++ b/qqq-sample-project/src/main/resources/static-site/index.html
@@ -0,0 +1,22 @@
+
+
+hello world
\ No newline at end of file
diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java
index e342f586..e3cd2762 100644
--- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java
+++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java
@@ -24,6 +24,7 @@ package com.kingsrook.sampleapp;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider;
import org.junit.jupiter.api.Test;
@@ -43,8 +44,9 @@ class SampleCliTest
@Test
void testExitSuccess() throws QException
{
- QContext.init(SampleMetaDataProvider.defineInstance(), new QSession());
- int exitCode = new SampleCli().run(new String[] { "--meta-data" });
+ QInstance qInstance = SampleMetaDataProvider.defineTestInstance();
+ QContext.init(qInstance, new QSession());
+ int exitCode = new SampleCli().run(qInstance, new String[] { "--meta-data" });
assertEquals(0, exitCode);
}
@@ -56,8 +58,9 @@ class SampleCliTest
@Test
void testNotExitSuccess() throws QException
{
- QContext.init(SampleMetaDataProvider.defineInstance(), new QSession());
- int exitCode = new SampleCli().run(new String[] { "asdfasdf" });
+ QInstance qInstance = SampleMetaDataProvider.defineTestInstance();
+ QContext.init(qInstance, new QSession());
+ int exitCode = new SampleCli().run(qInstance, new String[] { "asdfasdf" });
assertNotEquals(0, exitCode);
}
diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java
index e2b488ca..8f584ee9 100644
--- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java
+++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java
@@ -75,7 +75,7 @@ public class SampleMetaDataProviderTest
void beforeEach() throws Exception
{
primeTestDatabase("prime-test-database.sql");
- QContext.init(SampleMetaDataProvider.defineInstance(), new QSession());
+ QContext.init(SampleMetaDataProvider.defineTestInstance(), new QSession());
}
@@ -190,7 +190,7 @@ public class SampleMetaDataProviderTest
@Test
public void testGreetProcess() throws Exception
{
- QInstance qInstance = SampleMetaDataProvider.defineInstance();
+ QInstance qInstance = SampleMetaDataProvider.defineTestInstance();
QTableMetaData personTable = SampleMetaDataProvider.defineTablePerson();
RunProcessInput request = new RunProcessInput();
request.setProcessName(SampleMetaDataProvider.PROCESS_NAME_GREET);
@@ -216,7 +216,7 @@ public class SampleMetaDataProviderTest
@Test
public void testThrowProcess() throws Exception
{
- QInstance qInstance = SampleMetaDataProvider.defineInstance();
+ QInstance qInstance = SampleMetaDataProvider.defineTestInstance();
RunProcessInput request = new RunProcessInput();
request.setProcessName(SampleMetaDataProvider.PROCESS_NAME_SIMPLE_THROW);
request.addValue(SampleMetaDataProvider.ThrowerStep.FIELD_SLEEP_MILLIS, 10);
diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java
index 4b9af90c..f5599a79 100644
--- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java
+++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java
@@ -47,7 +47,7 @@ class RenderAllWidgetsTest
@Test
void test() throws QException
{
- QInstance qInstance = SampleMetaDataProvider.defineInstance();
+ QInstance qInstance = SampleMetaDataProvider.defineTestInstance();
QContext.init(qInstance, new QSession());
////////////////////////////////////////////////////////////////
diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java
index 4de7c550..5dc4245e 100644
--- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java
+++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java
@@ -82,7 +82,7 @@ class ClonePeopleTransformStepTest
@Test
void testProcessStep() throws QException
{
- QInstance qInstance = SampleMetaDataProvider.defineInstance();
+ QInstance qInstance = SampleMetaDataProvider.defineTestInstance();
QContext.init(qInstance, new QSession());
QueryInput queryInput = new QueryInput();