Merged dev into feature/workflows-support

This commit is contained in:
2025-05-19 15:59:24 -05:00
129 changed files with 7063 additions and 248 deletions

View File

@ -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
{

View File

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

View File

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

View File

@ -330,7 +330,21 @@ public class QInstanceEnricher
if(table.getFields() != null)
{
table.getFields().values().forEach(this::enrichField);
for(Map.Entry<String, QFieldMetaData> 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())
{

View File

@ -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<String> 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)

View File

@ -72,6 +72,18 @@ public class SecretsManagerUtils
** and write them to a .env file (backing up any pre-existing .env files first).
*******************************************************************************/
public static void writeEnvFromSecretsWithNamePrefix(String prefix) throws IOException
{
writeEnvFromSecretsWithNamePrefix(prefix, true);
}
/*******************************************************************************
** IF secret manager ENV vars are set,
** THEN lookup all secrets starting with the given prefix,
** and write them to a .env file (backing up any pre-existing .env files first).
*******************************************************************************/
public static void writeEnvFromSecretsWithNamePrefix(String prefix, boolean quoteValues) throws IOException
{
Optional<AWSSecretsManager> optionalSecretsManagerClient = getSecretsManagerClient();
if(optionalSecretsManagerClient.isPresent())
@ -91,7 +103,9 @@ public class SecretsManagerUtils
Optional<String> secretValue = getSecret(prefix, nameWithoutPrefix);
if(secretValue.isPresent())
{
String envLine = nameWithoutPrefix + "=" + secretValue.get();
String envLine = quoteValues
? nameWithoutPrefix + "=\"" + secretValue.get() + "\""
: nameWithoutPrefix + "=" + secretValue.get();
fullEnv.append(envLine).append('\n');
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T extends QMetaDataObject>
{
private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class);
private String fileName;
private List<LoadingProblem> problems = new ArrayList<>();
/***************************************************************************
**
***************************************************************************/
public T fileToMetaDataObject(QInstance qInstance, InputStream inputStream, String fileName) throws QMetaDataLoaderException
{
this.fileName = fileName;
Map<String, Object> map = fileToMap(inputStream, fileName);
LoadingContext loadingContext = new LoadingContext(fileName, "/");
return (mapToMetaDataObject(qInstance, map, loadingContext));
}
/***************************************************************************
**
***************************************************************************/
public abstract T mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException;
/***************************************************************************
**
***************************************************************************/
protected Map<String, Object> 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<String, Object> map, LoadingContext context)
{
Class<? extends QMetaDataObject> targetClass = targetObject.getClass();
Set<String> 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<String> 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<Object> 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<Object> 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<String, Object> mappedValueMap = new LinkedHashMap<>();
for(Object o : valueMap.entrySet())
{
try
{
@SuppressWarnings("unchecked")
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) 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<String, Object> map, String key)
//{
// if(map.containsKey(key))
// {
// if(map.get(key) instanceof List)
// {
// return (new ListOfMapOrMapOfMap((List<Map<String, Object>>) map.get(key)));
// }
// else if(map.get(key) instanceof Map)
// {
// return (new ListOfMapOrMapOfMap((Map<String, Map<String, Object>>) 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<Map<String, Object>> getListOfMap(Map<String, Object> map, String key)
//{
// if(map.containsKey(key))
// {
// if(map.get(key) instanceof List)
// {
// return (List<Map<String, Object>>) 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<String, Map<String, Object>> getMapOfMap(Map<String, Object> map, String key)
//{
// if(map.containsKey(key))
// {
// if(map.get(key) instanceof Map)
// {
// return (Map<String, Map<String, Object>>) 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<Map<String, Object>> listOf, Map<String, Map<String, Object>> mapOf)
//{
// /*******************************************************************************
// ** Constructor
// **
// *******************************************************************************/
// public ListOfMapOrMapOfMap(List<Map<String, Object>> listOf)
// {
// this(listOf, null);
// }
// /*******************************************************************************
// ** Constructor
// **
// *******************************************************************************/
// public ListOfMapOrMapOfMap(Map<String, Map<String, Object>> 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<LoadingProblem> getProblems()
{
return (problems);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QMetaDataObject>
{
private static final Memoization<AnyKey, List<Class<?>>> memoizedMetaDataObjectClasses = new Memoization<>();
/***************************************************************************
*
***************************************************************************/
public AbstractMetaDataLoader<?> getLoaderForFile(InputStream inputStream, String fileName) throws QMetaDataLoaderException
{
Map<String, Object> map = fileToMap(inputStream, fileName);
return (getLoaderForMap(map));
}
/***************************************************************************
*
***************************************************************************/
public AbstractMetaDataLoader<?> getLoaderForMap(Map<String, Object> 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<List<Class<?>>> 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<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
{
AbstractMetaDataLoader<?> loaderForMap = getLoaderForMap(map);
return loaderForMap.mapToMetaDataObject(qInstance, map, context);
}
}

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Pair<File, AbstractMetaDataLoader<?>>> 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<File, AbstractMetaDataLoader<?>> 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<Pair<File, AbstractMetaDataLoader<?>>> 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);
}
}
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<?>, Class<? extends AbstractMetaDataLoader<?>>> registeredLoaders = new HashMap<>();
private static final Map<String, Class<? extends AbstractMetaDataLoader<?>>> registeredLoadersByTargetSimpleName = new HashMap<>();
static
{
try
{
List<Class<?>> 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);
}
}

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T extends QMetaDataObject> extends AbstractMetaDataLoader<T>
{
private final Class<T> metaDataClass;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public GenericMetaDataLoader(Class<T> metaDataClass)
{
this.metaDataClass = metaDataClass;
}
/***************************************************************************
**
***************************************************************************/
@Override
public T mapToMetaDataObject(QInstance qInstance, Map<String, Object> 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));
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QStepMetaData>
{
private static final QLogger LOG = QLogger.getLogger(QStepDataLoader.class);
/***************************************************************************
**
***************************************************************************/
@Override
public QStepMetaData mapToMetaDataObject(QInstance qInstance, Map<String, Object> 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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<QTableMetaData>
{
private static final QLogger LOG = QLogger.getLogger(QTableMetaDataLoader.class);
/***************************************************************************
**
***************************************************************************/
@Override
public QTableMetaData mapToMetaDataObject(QInstance qInstance, Map<String, Object> map, LoadingContext context) throws QMetaDataLoaderException
{
QTableMetaData table = new QTableMetaData();
reflectivelyMap(qInstance, table, map, context);
// todo - handle QTableBackendDetails, based on backend's type
return (table);
}
}

View File

@ -0,0 +1,162 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<Class<? extends QProcessPayload>, QRecordEntityField> fieldMapping = new ListingHash<>();
/*******************************************************************************
** Build an entity of this QRecord type from a QRecord
**
*******************************************************************************/
public static <T extends QProcessPayload> T fromProcessState(Class<T> 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<QRecordEntityField> fieldList = getFieldList(this.getClass());
for(QRecordEntityField qRecordEntityField : fieldList)
{
Serializable value = processState.getValues().get(qRecordEntityField.getFieldName());
Object typedValue = qRecordEntityField.convertValueType(value);
qRecordEntityField.getSetter().invoke(this, typedValue);
}
}
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<Class<?>> allowedFieldTypes()
{
HashSet<Class<?>> classes = new HashSet<>(ReflectiveBeanLikeClassUtils.defaultAllowedTypes());
classes.add(Map.class);
classes.add(List.class);
return (classes);
}
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecordEntityField> getFieldList(Class<? extends QProcessPayload> c)
{
if(!fieldMapping.containsKey(c))
{
List<QRecordEntityField> fieldList = new ArrayList<>();
for(Method possibleGetter : c.getMethods())
{
if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, false, allowedFieldTypes()))
{
Optional<Method> 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));
}
}

View File

@ -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 extends QProcessPayload> T getProcessPayload(Class<T> payloadClass) throws QException
{
return QProcessPayload.fromProcessState(payloadClass, getProcessState());
}
}

View File

@ -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());
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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<QRecordEntityField> fieldList = new ArrayList<>();
for(Method possibleGetter : c.getMethods())
{
if(isGetter(possibleGetter))
if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, true))
{
Optional<Method> setter = getSetterForGetter(c, possibleGetter);
Optional<Method> setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter);
if(setter.isPresent())
{
String fieldName = getFieldNameFromGetter(possibleGetter);
String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter);
Optional<QField> fieldAnnotation = getQFieldAnnotation(c, fieldName);
if(fieldAnnotation.isPresent())
@ -378,19 +379,19 @@ public abstract class QRecordEntity
List<QRecordEntityAssociation> associationList = new ArrayList<>();
for(Method possibleGetter : c.getMethods())
{
if(isGetter(possibleGetter))
if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, true))
{
Optional<Method> setter = getSetterForGetter(c, possibleGetter);
Optional<Method> setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter);
if(setter.isPresent())
{
String fieldName = getFieldNameFromGetter(possibleGetter);
String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter);
Optional<QAssociation> 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)));
}
}

View File

@ -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)
{

View File

@ -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"),

View File

@ -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);
}

View File

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

View File

@ -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 extends QSupplementalInstanceMetaData> S of(QInstance qInstance, String name)
{
return ((S) qInstance.getSupplementalMetaData(name));
}
/***************************************************************************
**
***************************************************************************/
static <S extends QSupplementalInstanceMetaData> S ofOrWithNew(QInstance qInstance, String name, Supplier<S> supplier)
{
S s = (S) qInstance.getSupplementalMetaData(name);
if(s == null)
{
s = supplier.get();
s.addSelfToInstance(qInstance);
}
return (s);
}
}

View File

@ -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
{
/*******************************************************************************

View File

@ -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;

View File

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

View File

@ -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 //
//////////////////
}
}

View File

@ -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;

View File

@ -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")

View File

@ -0,0 +1,174 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.fields;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Field behavior that changes the whitespace of string values.
*******************************************************************************/
public enum WhiteSpaceBehavior implements FieldBehavior<WhiteSpaceBehavior>, FieldBehaviorForFrontend, FieldFilterBehavior<WhiteSpaceBehavior>
{
NONE(null),
REMOVE_ALL_WHITESPACE((String s) -> s.chars().filter(c -> !Character.isWhitespace(c)).collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString()),
TRIM((String s) -> s.trim()),
TRIM_LEFT((String s) -> s.stripLeading()),
TRIM_RIGHT((String s) -> s.stripTrailing());
private final Function<String, String> function;
/*******************************************************************************
**
*******************************************************************************/
WhiteSpaceBehavior(Function<String, String> function)
{
this.function = function;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public WhiteSpaceBehavior getDefault()
{
return (NONE);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
if(this.equals(NONE))
{
return;
}
switch(this)
{
case REMOVE_ALL_WHITESPACE, TRIM, TRIM_LEFT, TRIM_RIGHT -> applyFunction(recordList, table, field);
default -> throw new IllegalStateException("Unexpected enum value: " + this);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void applyFunction(List<QRecord> recordList, QTableMetaData table, QFieldMetaData field)
{
String fieldName = field.getName();
for(QRecord record : CollectionUtils.nonNullList(recordList))
{
String value = record.getValueString(fieldName);
if(value != null && function != null)
{
record.setValue(fieldName, function.apply(value));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable applyToFilterCriteriaValue(Serializable value, QInstance instance, QTableMetaData table, QFieldMetaData field)
{
if(this.equals(NONE) || function == null)
{
return (value);
}
if(value instanceof String s)
{
String newValue = function.apply(s);
if(!Objects.equals(value, newValue))
{
return (newValue);
}
}
return (value);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean allowMultipleBehaviorsOfThisType()
{
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<String> validateBehaviorConfiguration(QTableMetaData tableMetaData, QFieldMetaData fieldMetaData)
{
if(this == NONE)
{
return Collections.emptyList();
}
List<String> errors = new ArrayList<>();
String errorSuffix = " field [" + fieldMetaData.getName() + "] in table [" + tableMetaData.getName() + "]";
if(fieldMetaData.getType() != null)
{
if(!fieldMetaData.getType().isStringLike())
{
errors.add("A WhiteSpaceBehavior was a applied to a non-String-like field:" + errorSuffix);
}
}
return (errors);
}
}

View File

@ -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;

View File

@ -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
{
/*******************************************************************************
**

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<QStepMetaData> stepList)
{
this.stepList = stepList;
if(stepList == null)
{
this.stepList = null;
this.steps = null;
}
else
{
this.stepList = new ArrayList<>();
this.steps = new HashMap<>();
}
withStepList(stepList);
}

View File

@ -185,4 +185,25 @@ public class QStateMachineStep extends QStepMetaData
return (rs);
}
/*******************************************************************************
** Setter for subSteps
*******************************************************************************/
public void setSubSteps(List<QStepMetaData> subSteps)
{
this.subSteps = subSteps;
}
/*******************************************************************************
** Fluent setter for subSteps
*******************************************************************************/
public QStateMachineStep withSubSteps(List<QStepMetaData> subSteps)
{
this.subSteps = subSteps;
return (this);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<String> fieldNames;
private String label;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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()));
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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<String, String> context)
public QSession createSession(QInstance qInstance, Map<String, String> 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";
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, String> getAccessTokenFromSessionUUIDMemoization = new Memoization<String, String>()
.withTimeout(Duration.of(1, ChronoUnit.MINUTES))
.withMaxSize(1000);
private static final Memoization<String, OIDCProviderMetadata> oidcProviderMetadataMemoization = new Memoization<String, OIDCProviderMetadata>()
.withMayStoreNullValues(false);
/***************************************************************************
**
***************************************************************************/
@Override
public QSession createSession(QInstance qInstance, Map<String, String> 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<String> 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()));
}
}

View File

@ -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);

View File

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

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.processes.utils;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -51,13 +52,14 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
*******************************************************************************/
public class RecordLookupHelper
{
private Map<String, Map<Serializable, QRecord>> recordMaps = new HashMap<>();
private Map<String, Map<Serializable, QRecord>> recordMaps;
private Map<String, Map<Map<String, Serializable>, QRecord>> uniqueKeyMaps = new HashMap<>();
private Map<String, Map<Map<String, Serializable>, QRecord>> uniqueKeyMaps;
private Set<String> preloadedKeys = new HashSet<>();
private Set<String> preloadedKeys;
private Set<Pair<String, String>> disallowedOneOffLookups = new HashSet<>();
private Set<Pair<String, String>> disallowedOneOffLookups;
private boolean useSynchronizedCollections;
@ -67,6 +69,33 @@ public class RecordLookupHelper
*******************************************************************************/
public RecordLookupHelper()
{
this(false);
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public RecordLookupHelper(boolean useSynchronizedCollections)
{
this.useSynchronizedCollections = useSynchronizedCollections;
if(useSynchronizedCollections)
{
recordMaps = Collections.synchronizedMap(new HashMap<>());
uniqueKeyMaps = Collections.synchronizedMap(new HashMap<>());
preloadedKeys = Collections.synchronizedSet(new HashSet<>());
disallowedOneOffLookups = Collections.synchronizedSet(new HashSet<>());
}
else
{
recordMaps = new HashMap<>();
uniqueKeyMaps = new HashMap<>();
preloadedKeys = new HashSet<>();
disallowedOneOffLookups = new HashSet<>();
}
}
@ -77,7 +106,7 @@ public class RecordLookupHelper
public QRecord getRecordByUniqueKey(String tableName, Map<String, Serializable> uniqueKey) throws QException
{
String mapKey = tableName + "." + uniqueKey.keySet().stream().sorted().collect(Collectors.joining(","));
Map<Map<String, Serializable>, QRecord> recordMap = uniqueKeyMaps.computeIfAbsent(mapKey, (k) -> new HashMap<>());
Map<Map<String, Serializable>, QRecord> recordMap = uniqueKeyMaps.computeIfAbsent(mapKey, (k) -> useSynchronizedCollections ? Collections.synchronizedMap(new HashMap<>()) : new HashMap<>());
if(!recordMap.containsKey(uniqueKey))
{
@ -96,7 +125,7 @@ public class RecordLookupHelper
public QRecord getRecordByKey(String tableName, String keyFieldName, Serializable key) throws QException
{
String mapKey = tableName + "." + keyFieldName;
Map<Serializable, QRecord> recordMap = recordMaps.computeIfAbsent(mapKey, (k) -> new HashMap<>());
Map<Serializable, QRecord> recordMap = recordMaps.computeIfAbsent(mapKey, (k) -> useSynchronizedCollections ? Collections.synchronizedMap(new HashMap<>()) : new HashMap<>());
////////////////////////////////////////////////////////////
// make sure we have they key object in the expected type //
@ -150,7 +179,7 @@ public class RecordLookupHelper
public void preloadRecords(String tableName, String keyFieldName, QQueryFilter filter) throws QException
{
String mapKey = tableName + "." + keyFieldName;
Map<Serializable, QRecord> tableMap = recordMaps.computeIfAbsent(mapKey, s -> new HashMap<>());
Map<Serializable, QRecord> tableMap = recordMaps.computeIfAbsent(mapKey, s -> useSynchronizedCollections ? Collections.synchronizedMap(new HashMap<>()) : new HashMap<>());
tableMap.putAll(GeneralProcessUtils.loadTableToMap(tableName, keyFieldName, filter));
}
@ -170,7 +199,7 @@ public class RecordLookupHelper
}
String mapKey = tableName + "." + keyFieldName;
Map<Serializable, QRecord> tableMap = recordMaps.computeIfAbsent(mapKey, s -> new HashMap<>());
Map<Serializable, QRecord> tableMap = recordMaps.computeIfAbsent(mapKey, s -> useSynchronizedCollections ? Collections.synchronizedMap(new HashMap<>()) : new HashMap<>());
QQueryFilter filter = new QQueryFilter(new QFilterCriteria(keyFieldName, QCriteriaOperator.IN, inList));
tableMap.putAll(GeneralProcessUtils.loadTableToMap(tableName, keyFieldName, filter));

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.state;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@ -58,7 +59,7 @@ public class InMemoryStateProvider implements StateProviderInterface
*******************************************************************************/
private InMemoryStateProvider()
{
this.map = new HashMap<>();
this.map = Collections.synchronizedMap(new HashMap<>());
///////////////////////////////////////////////////////////
// Start a single thread executor to handle the cleaning //

View File

@ -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<Class<?>> getClassesContainingNameAndOfType(String nameContains, Class<?> type) throws IOException
{
List<Class<?>> 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);
}
/*******************************************************************************
**
*******************************************************************************/

View File

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

View File

@ -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)));
}
}

View File

@ -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());
}
}
}

View File

@ -24,6 +24,7 @@
<!-- c3p0 -->
<Logger name="com.mchange.v2" level="INFO" />
<Logger name="org.quartz" level="INFO" />
<Logger name="org.apache.http" level="INFO"/>
<Logger name="liquibase" level="INFO" />
<Logger name="com.amazonaws" level="INFO" />
<Root level="all">