Compare commits

...

26 Commits

Author SHA1 Message Date
a5c65b9e67 Test coverage on new javalin routing classes 2025-01-30 20:46:33 -06:00
48fbb3d054 Update setStepList to properly fully replace both step list and map 2025-01-30 20:46:04 -06:00
bcca710316 Javalin process-based custom router; javalin meta-data to define routers 2025-01-30 19:13:32 -06:00
6d749e9df6 First version of loading process meta-data via loader (steps needed discriminating loader) 2025-01-30 19:11:39 -06:00
81ffe1a286 checkstyle 2025-01-23 10:38:56 -06:00
6b49abb749 Checkpoint - serving static site 2025-01-23 10:11:47 -06:00
efb47b9cd6 Checkpoint - yaml-meta data and sample server 2025-01-23 10:09:03 -06:00
29f2feb321 Start support for static-file routing 2025-01-23 10:08:42 -06:00
3537d2cfd1 make QJavalinMetaData implements QSupplementalInstanceMetaData 2025-01-23 10:08:30 -06:00
634abe3822 Checkpoint on loaders tests 2025-01-23 09:51:29 -06:00
93c7fbca25 Checkpoint on loaders 2025-01-23 09:39:31 -06:00
ea40197893 more QQQApplication implementations 2025-01-23 09:37:21 -06:00
38293b81d7 Switch QSupplementalInstanceMetaData to interface instead of abstract class; remove getType in favor of getName from its base class, TopLevelMetaDataInterface; 2025-01-23 09:35:55 -06:00
7b141c3f5b Add implements QMetaDataObject 2025-01-23 09:33:34 -06:00
502095002c Add getClassesContainingNameAndOfType 2025-01-23 09:32:57 -06:00
42a8d37493 add methods: maskAndTruncate; nCopies; nCopiesWithGlue 2025-01-23 09:32:46 -06:00
6725704b13 Merged dev into feature/meta-data-loaders 2025-01-17 19:12:48 -06:00
48ac6a0a4f Checkstyle 2025-01-16 19:51:57 -06:00
3f4d11b22a Checkpoint - class-detecting loader handling generic loaders; generic loader created & working; Loader registry moved to its own class; 2025-01-16 14:08:32 -06:00
f147516e45 Make tests passing 2024-12-23 11:44:55 -06:00
f3fe8a3c73 Checkstyle! 2024-12-23 11:39:09 -06:00
71dcf231db Checkstyle! 2024-12-23 11:34:22 -06:00
a20efabcf2 Initial checkin 2024-12-23 11:33:09 -06:00
00b72e0338 In enrichTable, set name in QFieldMetaData based on its key in the fields map, if it wasn't otherwise set. 2024-12-23 11:31:11 -06:00
b979e6545a Mark class as implementing QMetaDataObject 2024-12-23 11:30:27 -06:00
7982cad794 Initial build of classes to load meta-data from yaml or json files 2024-12-23 11:29:30 -06:00
71 changed files with 3508 additions and 58 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

@ -289,7 +289,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

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

@ -31,6 +31,7 @@ import java.util.Objects;
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;
@ -40,7 +41,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

@ -1245,7 +1245,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

@ -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
public class QCodeReference implements Serializable, QMetaDataObject
{
private String name;
private QCodeType codeType;

View File

@ -40,6 +40,7 @@ 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;
@ -54,7 +55,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);

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
public class QIcon implements 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

@ -320,12 +320,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

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

@ -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;
@ -475,4 +476,59 @@ public class StringUtils
return (s);
}
/*******************************************************************************
**
*******************************************************************************/
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

@ -43,6 +43,9 @@ class AbstractMetaDataProducerBasedQQQApplicationTest extends BaseTest
{
QInstance qInstance = new TestApplication().defineQInstance();
assertEquals(1, qInstance.getTables().size());
assertEquals("fromProducer", qInstance.getTables().get("fromProducer").getName());
assertEquals(1, qInstance.getProcesses().size());
assertEquals("fromProducer", qInstance.getProcesses().get("fromProducer").getName());
}

View File

@ -0,0 +1,66 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.instances;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.producers.TestMetaDataProducer;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for MetaDataProducerBasedQQQApplication
*******************************************************************************/
class MetaDataProducerBasedQQQApplicationTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
QInstance qInstance = new MetaDataProducerBasedQQQApplication(getClass().getPackage().getName() + ".producers").defineQInstance();
assertEquals(1, qInstance.getTables().size());
assertEquals("fromProducer", qInstance.getTables().get("fromProducer").getName());
assertEquals(1, qInstance.getProcesses().size());
assertEquals("fromProducer", qInstance.getProcesses().get("fromProducer").getName());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testConstructorThatTakeClass() throws QException
{
QInstance qInstance = new MetaDataProducerBasedQQQApplication(TestMetaDataProducer.class).defineQInstance();
assertEquals(1, qInstance.getTables().size());
assertEquals("fromProducer", qInstance.getTables().get("fromProducer").getName());
assertEquals(1, qInstance.getProcesses().size());
assertEquals("fromProducer", qInstance.getProcesses().get("fromProducer").getName());
}
}

View File

@ -0,0 +1,136 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.instances.loaders;
import java.nio.charset.StandardCharsets;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.instances.loaders.implementations.GenericMetaDataLoader;
import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for AbstractMetaDataLoader
*******************************************************************************/
class AbstractMetaDataLoaderTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testVariousPropertyTypes() throws QMetaDataLoaderException
{
QProcessMetaData process = new GenericMetaDataLoader<>(QProcessMetaData.class).fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: QProcessMetaData
version: 1
name: myProcess
tableName: someTable
maxInputRecords: 1
isHidden: true
""", StandardCharsets.UTF_8), "myProcess.yaml");
assertEquals("myProcess", process.getName());
assertEquals("someTable", process.getTableName());
assertEquals(1, process.getMaxInputRecords());
assertTrue(process.getIsHidden());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testProblems() throws QMetaDataLoaderException
{
{
QTableMetaDataLoader loader = new QTableMetaDataLoader();
loader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: QTableMetaData
version: 1.0
name: myTable
something: foo
isHidden: hi
icon:
name: account_tree
size: big
weight: bold
fields:
id:
type: number
uniqueKeys: sure!
""", StandardCharsets.UTF_8), "myTable.yaml");
for(LoadingProblem problem : loader.getProblems())
{
System.out.println(problem);
}
}
{
GenericMetaDataLoader<QProcessMetaData> loader = new GenericMetaDataLoader<>(QProcessMetaData.class);
loader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: QProcessMetaData
version: 1.0
name: myProcess
maxInputRecords: many
""", StandardCharsets.UTF_8), "myProcess.yaml");
for(LoadingProblem problem : loader.getProblems())
{
System.out.println(problem);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testEnvironmentValues() throws QMetaDataLoaderException
{
System.setProperty("myProcess.tableName", "someTable");
System.setProperty("myProcess.maxInputRecords", "47");
GenericMetaDataLoader<QProcessMetaData> loader = new GenericMetaDataLoader<>(QProcessMetaData.class);
QProcessMetaData processMetaData = loader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: QProcessMetaData
version: 1.0
name: myProcess
tableName: ${prop.myProcess.tableName}
maxInputRecords: ${prop.myProcess.maxInputRecords}
""", StandardCharsets.UTF_8), "myProcess.yaml");
assertEquals("someTable", processMetaData.getTableName());
assertEquals(47, processMetaData.getMaxInputRecords());
}
}

View File

@ -0,0 +1,115 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.instances.loaders;
import java.nio.charset.StandardCharsets;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for ClassDetectingMetaDataLoader
*******************************************************************************/
class ClassDetectingMetaDataLoaderTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testBasicSuccess() throws QMetaDataLoaderException
{
QMetaDataObject qMetaDataObject = new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: QTableMetaData
version: 1
name: myTable
backendName: someBackend
""", StandardCharsets.UTF_8), "myTable.yaml");
assertThat(qMetaDataObject).isInstanceOf(QTableMetaData.class);
QTableMetaData qTableMetaData = (QTableMetaData) qMetaDataObject;
assertEquals("myTable", qTableMetaData.getName());
assertEquals("someBackend", qTableMetaData.getBackendName());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testProcess() throws QMetaDataLoaderException
{
QMetaDataObject qMetaDataObject = new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: QProcessMetaData
version: 1
name: myProcess
tableName: someTable
""", StandardCharsets.UTF_8), "myProcess.yaml");
assertThat(qMetaDataObject).isInstanceOf(QProcessMetaData.class);
QProcessMetaData qProcessMetaData = (QProcessMetaData) qMetaDataObject;
assertEquals("myProcess", qProcessMetaData.getName());
assertEquals("someTable", qProcessMetaData.getTableName());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testUnknownClassFails()
{
assertThatThrownBy(() -> new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: ya whatever
version: 1
name: myTable
""", StandardCharsets.UTF_8), "whatever.yaml"))
.isInstanceOf(QMetaDataLoaderException.class)
.hasMessageContaining("Unexpected class");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testMissingClassAttributeFails()
{
assertThatThrownBy(() -> new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
version: 1
name: myTable
""", StandardCharsets.UTF_8), "aTable.yaml"))
.isInstanceOf(QMetaDataLoaderException.class)
.hasMessageContaining("[class] attribute was not specified");
}
}

View File

@ -0,0 +1,111 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.instances.loaders;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for MetaDataLoaderHelper
*******************************************************************************/
class MetaDataLoaderHelperTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws Exception
{
Path tempDirectory = Files.createTempDirectory(getClass().getSimpleName());
writeFile("myTable", ".yaml", tempDirectory, """
class: QTableMetaData
version: 1
name: myTable
label: This is My Table
primaryKeyField: id
fields:
id:
name: id
type: INTEGER
name:
name: name
type: STRING
createDate:
name: createDate
type: DATE_TIME
""");
writeFile("yourTable", ".yaml", tempDirectory, """
class: QTableMetaData
version: 1
name: yourTable
label: Someone else's table
primaryKeyField: id
fields:
id:
name: id
type: INTEGER
name:
name: name
type: STRING
""");
QInstance qInstance = new QInstance();
MetaDataLoaderHelper.processAllMetaDataFilesInDirectory(qInstance, tempDirectory.toFile().getAbsolutePath());
assertEquals(2, qInstance.getTables().size());
QTableMetaData myTable = qInstance.getTable("myTable");
assertEquals("This is My Table", myTable.getLabel());
assertEquals(3, myTable.getFields().size());
assertEquals("id", myTable.getField("id").getName());
assertEquals(QFieldType.INTEGER, myTable.getField("id").getType());
QTableMetaData yourTable = qInstance.getTable("yourTable");
assertEquals("Someone else's table", yourTable.getLabel());
assertEquals(2, yourTable.getFields().size());
}
/***************************************************************************
**
***************************************************************************/
void writeFile(String prefix, String suffix, Path directory, String content) throws IOException
{
FileUtils.writeStringToFile(File.createTempFile(prefix, suffix, directory.toFile()), content, StandardCharsets.UTF_8);
}
}

View File

@ -0,0 +1,103 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.instances.loaders;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for loading a QProcessMetaData (doesn't need its own loader yet,
** but is still a valuable high-level test target).
*******************************************************************************/
class QProcessMetaDataLoaderTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testYaml() throws QMetaDataLoaderException
{
ClassDetectingMetaDataLoader metaDataLoader = new ClassDetectingMetaDataLoader();
QProcessMetaData process = (QProcessMetaData) metaDataLoader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: QProcessMetaData
version: 1.0
name: myProcess
stepList:
- name: myBackendStep
stepType: backend
code:
name: com.kingsrook.test.processes.MyBackendStep
- name: myFrontendStep
stepType: frontend
components:
- type: HELP_TEXT
values:
foo: bar
- type: VIEW_FORM
viewFields:
- name: myField
type: STRING
- name: yourField
type: DATE
""", StandardCharsets.UTF_8), "myProcess.yaml");
CollectionUtils.nonNullList(metaDataLoader.getProblems()).forEach(System.out::println);
assertEquals("myProcess", process.getName());
assertEquals(2, process.getAllSteps().size());
QBackendStepMetaData myBackendStep = process.getBackendStep("myBackendStep");
assertNotNull(myBackendStep, "myBackendStep should not be null");
// todo - propagate this? assertEquals("myBackendStep", myBackendStep.getName());
assertEquals("com.kingsrook.test.processes.MyBackendStep", myBackendStep.getCode().getName());
QFrontendStepMetaData myFrontendStep = process.getFrontendStep("myFrontendStep");
assertNotNull(myFrontendStep, "myFrontendStep should not be null");
assertEquals(2, myFrontendStep.getComponents().size());
assertEquals(QComponentType.HELP_TEXT, myFrontendStep.getComponents().get(0).getType());
assertEquals(Map.of("foo", "bar"), myFrontendStep.getComponents().get(0).getValues());
assertEquals(QComponentType.VIEW_FORM, myFrontendStep.getComponents().get(1).getType());
assertEquals(2, myFrontendStep.getViewFields().size());
assertEquals("myField", myFrontendStep.getViewFields().get(0).getName());
assertEquals(QFieldType.STRING, myFrontendStep.getViewFields().get(0).getType());
assertEquals("yourField", myFrontendStep.getViewFields().get(1).getName());
assertEquals(QFieldType.DATE, myFrontendStep.getViewFields().get(1).getType());
}
}

View File

@ -0,0 +1,202 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.instances.loaders;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import com.kingsrook.qqq.backend.core.utils.YamlUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for QTableMetaDataLoader
*******************************************************************************/
class QTableMetaDataLoaderTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
@Disabled("Not quite yet passing - is a good goal to get to though!")
void testToYaml() throws QMetaDataLoaderException
{
QTableMetaData expectedTable = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
String expectedYaml = YamlUtils.toYaml(expectedTable);
QTableMetaData actualTable = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(expectedYaml, StandardCharsets.UTF_8), "person.yaml");
String actualYaml = YamlUtils.toYaml(actualTable);
assertEquals(expectedYaml, actualYaml);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testYaml() throws QMetaDataLoaderException
{
QTableMetaData table = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: QTableMetaData
version: 1.0
name: myTable
icon:
name: account_tree
fields:
id:
name: id
type: INTEGER
name:
name: name
type: STRING
uniqueKeys:
- label: Name
fieldNames:
- name
associations:
- name: A1
associatedTableName: yourTable
joinName: myTableJoinYourTable
- name: A2
associatedTableName: theirTable
joinName: myTableJoinTheirTable
permissionRules:
level: READ_WRITE_PERMISSIONS
denyBehavior: HIDDEN
permissionBaseName: myTablePermissions
customPermissionChecker:
name: com.kingsrook.SomeChecker
codeType: JAVA
## todo recordSecurityLocks
## todo auditRules
## todo backendDetails
## todo automationDetails
sections:
- name: identity
label: Identity
icon:
name: badge
tier: T1
fieldNames:
- id
- firstName
- lastName
customizers:
postQueryRecord:
name: com.kingsrook.SomePostQuery
codeType: JAVA
preDeleteRecord:
name: com.kingsrook.SomePreDelete
codeType: JAVA
disabledCapabilities:
- TABLE_COUNT
- QUERY_STATS
""", StandardCharsets.UTF_8), "myTable.yaml");
assertEquals("myTable", table.getName());
assertEquals(2, table.getFields().size());
// assertEquals("id", table.getFields().get("id").getName());
assertEquals(QFieldType.INTEGER, table.getFields().get("id").getType());
// assertEquals("name", table.getFields().get("name").getName());
assertEquals(QFieldType.STRING, table.getFields().get("name").getType());
assertNotNull(table.getIcon());
assertEquals("account_tree", table.getIcon().getName());
assertEquals(1, table.getUniqueKeys().size());
assertEquals(List.of("name"), table.getUniqueKeys().get(0).getFieldNames());
assertEquals("Name", table.getUniqueKeys().get(0).getLabel());
assertEquals(2, table.getAssociations().size());
assertEquals("A1", table.getAssociations().get(0).getName());
assertEquals("theirTable", table.getAssociations().get(1).getAssociatedTableName());
assertNotNull(table.getPermissionRules());
assertEquals(PermissionLevel.READ_WRITE_PERMISSIONS, table.getPermissionRules().getLevel());
assertEquals(DenyBehavior.HIDDEN, table.getPermissionRules().getDenyBehavior());
assertEquals("myTablePermissions", table.getPermissionRules().getPermissionBaseName());
assertNotNull(table.getPermissionRules().getCustomPermissionChecker());
assertEquals("com.kingsrook.SomeChecker", table.getPermissionRules().getCustomPermissionChecker().getName());
assertEquals(QCodeType.JAVA, table.getPermissionRules().getCustomPermissionChecker().getCodeType());
assertEquals(1, table.getSections().size());
assertEquals("identity", table.getSections().get(0).getName());
assertEquals(Tier.T1, table.getSections().get(0).getTier());
assertEquals(List.of("id", "firstName", "lastName"), table.getSections().get(0).getFieldNames());
assertEquals(2, table.getCustomizers().size());
assertEquals("com.kingsrook.SomePostQuery", table.getCustomizers().get(TableCustomizers.POST_QUERY_RECORD.getRole()).getName());
assertEquals("com.kingsrook.SomePreDelete", table.getCustomizers().get(TableCustomizers.PRE_DELETE_RECORD.getRole()).getName());
assertEquals(Set.of(Capability.TABLE_COUNT, Capability.QUERY_STATS), table.getDisabledCapabilities());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testSimpleJson() throws QMetaDataLoaderException
{
QTableMetaData table = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
{
"class": "QTableMetaData",
"version": "1.0",
"name": "myTable",
"fields":
{
"id": {"name": "id", "type": "INTEGER"},
"name": {"name": "name", "type": "STRING"}
}
}
""", StandardCharsets.UTF_8), "myTable.json");
assertEquals("myTable", table.getName());
assertEquals(2, table.getFields().size());
assertEquals("id", table.getFields().get("id").getName());
assertEquals(QFieldType.INTEGER, table.getFields().get("id").getType());
assertEquals("name", table.getFields().get("name").getName());
assertEquals(QFieldType.STRING, table.getFields().get("name").getType());
}
}

View File

@ -0,0 +1,81 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.instances.loaders.implementations;
import java.nio.charset.StandardCharsets;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext;
import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*******************************************************************************
** Unit test for GenericMetaDataLoader - providing coverage for AbstractMetaDataLoader.
*******************************************************************************/
class GenericMetaDataLoaderTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testProcess() throws QMetaDataLoaderException
{
////////////////////////////////////////////////////////////////////////////////
// trying to get some coverage of various types in here (for Abstract loader) //
////////////////////////////////////////////////////////////////////////////////
QProcessMetaData process = new GenericMetaDataLoader<>(QProcessMetaData.class).fileToMetaDataObject(new QInstance(), IOUtils.toInputStream("""
class: QProcessMetaData
version: 1
name: myProcess
tableName: someTable
maxInputRecords: 1
isHidden: true
""", StandardCharsets.UTF_8), "myProcess.yaml");
assertEquals("myProcess", process.getName());
assertEquals("someTable", process.getTableName());
assertEquals(1, process.getMaxInputRecords());
assertTrue(process.getIsHidden());
}
/*******************************************************************************
** just here for coverage of this class, as we're failing to hit it otherwise.
*******************************************************************************/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
void testNoValueException()
{
assertThatThrownBy(() -> new GenericMetaDataLoader(QBackendMetaData.class).reflectivelyMapValue(new QInstance(), null, GenericMetaDataLoaderTest.class, "rawValue", new LoadingContext("test.yaml", "/")));
}
}

View File

@ -0,0 +1,47 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.instances.producers.subpackage;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class TestProcessMetaDataProducer implements MetaDataProducerInterface<QProcessMetaData>
{
/***************************************************************************
**
***************************************************************************/
@Override
public QProcessMetaData produce(QInstance qInstance) throws QException
{
return new QProcessMetaData().withName("fromProducer");
}
}

View File

@ -333,4 +333,76 @@ class StringUtilsTest extends BaseTest
assertEquals("a", StringUtils.emptyToNull("a"));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNCopies()
{
assertEquals("", StringUtils.nCopies(0, "a"));
assertEquals("a", StringUtils.nCopies(1, "a"));
assertEquals("aa", StringUtils.nCopies(2, "a"));
assertEquals("ab ab ab ", StringUtils.nCopies(3, "ab "));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNCopiesWithGlue()
{
assertEquals("", StringUtils.nCopiesWithGlue(0, "a", ""));
assertEquals("", StringUtils.nCopiesWithGlue(0, "a", ","));
assertEquals("a", StringUtils.nCopiesWithGlue(1, "a", ","));
assertEquals("aa", StringUtils.nCopiesWithGlue(2, "a", ""));
assertEquals("a,a", StringUtils.nCopiesWithGlue(2, "a", ","));
assertEquals("ab ab ab", StringUtils.nCopiesWithGlue(3, "ab", " "));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testMaskAndTruncate()
{
assertEquals("", StringUtils.maskAndTruncate(null));
assertEquals("", StringUtils.maskAndTruncate(""));
assertEquals("** MASKED **", StringUtils.maskAndTruncate("1"));
assertEquals("** MASKED **", StringUtils.maskAndTruncate("12"));
assertEquals("** MASKED **", StringUtils.maskAndTruncate("123"));
assertEquals("** MASKED **", StringUtils.maskAndTruncate("1234"));
assertEquals("** MASKED **", StringUtils.maskAndTruncate("12345"));
assertEquals("** MASKED **", StringUtils.maskAndTruncate("123456"));
assertEquals("** MASKED **", StringUtils.maskAndTruncate("1234567"));
assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("12345678"));
assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("123456789"));
assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("1234567890"));
assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("12345678901"));
assertEquals("1234** MASKED **9012", StringUtils.maskAndTruncate("123456789012"));
assertEquals("1234** MASKED **6789", StringUtils.maskAndTruncate("123456789" + StringUtils.nCopies(100, "xyz") + "123456789"));
assertEquals("***", StringUtils.maskAndTruncate("12", "***", 3, 1));
assertEquals("1***3", StringUtils.maskAndTruncate("123", "***", 3, 1));
assertEquals("1***4", StringUtils.maskAndTruncate("1234", "***", 3, 1));
assertEquals("1***5", StringUtils.maskAndTruncate("12345", "***", 3, 1));
assertEquals("12***", StringUtils.maskAndTruncate("12345", "***", 3, 2));
assertEquals("12***56", StringUtils.maskAndTruncate("123456", "***", 3, 2));
assertEquals("***", StringUtils.maskAndTruncate("12", "***", 3, 4));
assertEquals("***", StringUtils.maskAndTruncate("123", "***", 3, 4));
assertEquals("***", StringUtils.maskAndTruncate("1234", "***", 3, 4));
assertEquals("***", StringUtils.maskAndTruncate("12345", "***", 3, 4));
assertEquals("***", StringUtils.maskAndTruncate("12345", "***", 3, 4));
assertEquals("***", StringUtils.maskAndTruncate("123456", "***", 3, 4));
assertEquals("1234***", StringUtils.maskAndTruncate("1234567890", "***", 3, 4));
assertEquals("1234***", StringUtils.maskAndTruncate("12345678901", "***", 3, 4));
assertEquals("1234***9012", StringUtils.maskAndTruncate("123456789012", "***", 3, 4));
}
}

View File

@ -34,7 +34,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ApiInstanceMetaDataContainer extends QSupplementalInstanceMetaData
public class ApiInstanceMetaDataContainer implements QSupplementalInstanceMetaData
{
private Map<String, ApiInstanceMetaData> apis;
@ -71,17 +71,6 @@ public class ApiInstanceMetaDataContainer extends QSupplementalInstanceMetaData
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getType()
{
return (ApiSupplementType.NAME);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -208,7 +208,7 @@ public class QJavalinImplementation
*******************************************************************************/
public QJavalinImplementation(QInstance qInstance) throws QInstanceValidationException
{
this(qInstance, new QJavalinMetaData());
this(qInstance, QJavalinMetaData.ofOrWithNew(qInstance));
}

View File

@ -22,25 +22,65 @@
package com.kingsrook.qqq.backend.javalin;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import org.apache.logging.log4j.Level;
/*******************************************************************************
** MetaData specific to a QQQ Javalin server.
*******************************************************************************/
public class QJavalinMetaData
public class QJavalinMetaData implements QSupplementalInstanceMetaData
{
public static final String NAME = "javalin";
private String uploadedFileArchiveTableName;
private boolean loggerDisabled = false;
// todo - should be a code reference!!
private Function<QJavalinAccessLogger.LogEntry, Boolean> logFilter;
private boolean queryWithoutLimitAllowed = false;
private Integer queryWithoutLimitDefault = 1000;
private Level queryWithoutLimitLogLevel = Level.INFO;
private List<JavalinRouteProviderMetaData> routeProviders;
/***************************************************************************
**
***************************************************************************/
@Override
public String getName()
{
return (NAME);
}
/*******************************************************************************
**
*******************************************************************************/
public static QJavalinMetaData of(QInstance qInstance)
{
return QSupplementalInstanceMetaData.of(qInstance, NAME);
}
/*******************************************************************************
**
*******************************************************************************/
public static QJavalinMetaData ofOrWithNew(QInstance qInstance)
{
return QSupplementalInstanceMetaData.ofOrWithNew(qInstance, NAME, QJavalinMetaData::new);
}
/*******************************************************************************
@ -241,4 +281,35 @@ public class QJavalinMetaData
return (this);
}
/*******************************************************************************
** Getter for routeProviders
*******************************************************************************/
public List<JavalinRouteProviderMetaData> getRouteProviders()
{
return (this.routeProviders);
}
/*******************************************************************************
** Setter for routeProviders
*******************************************************************************/
public void setRouteProviders(List<JavalinRouteProviderMetaData> routeProviders)
{
this.routeProviders = routeProviders;
}
/*******************************************************************************
** Fluent setter for routeProviders
*******************************************************************************/
public QJavalinMetaData withRouteProviders(List<JavalinRouteProviderMetaData> routeProviders)
{
this.routeProviders = routeProviders;
return (this);
}
}

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.middleware.javalin;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
@ -31,11 +33,17 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.backend.javalin.QJavalinMetaData;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouter;
import com.kingsrook.qqq.middleware.javalin.routeproviders.SimpleFileSystemDirectoryRouter;
import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import io.javalin.Javalin;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.Context;
import org.apache.commons.lang.BooleanUtils;
import org.eclipse.jetty.util.resource.Resource;
@ -97,6 +105,12 @@ public class QApplicationJavalinServer
{
QInstance qInstance = application.defineValidatedQInstance();
QJavalinMetaData qJavalinMetaData = QJavalinMetaData.of(qInstance);
if(qJavalinMetaData != null)
{
addRouteProvidersFromMetaData(qJavalinMetaData);
}
service = Javalin.create(config ->
{
if(serveFrontendMaterialDashboard)
@ -110,7 +124,7 @@ public class QApplicationJavalinServer
////////////////////////////////////////////////////////////////////////////////////////
try(Resource resource = Resource.newClassPathResource("/material-dashboard-overlay"))
{
if(resource !=null)
if(resource != null)
{
config.staticFiles.add("/material-dashboard-overlay");
}
@ -166,7 +180,14 @@ public class QApplicationJavalinServer
for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
{
routeProvider.setQInstance(qInstance);
config.router.apiBuilder(routeProvider.getJavalinEndpointGroup());
EndpointGroup javalinEndpointGroup = routeProvider.getJavalinEndpointGroup();
if(javalinEndpointGroup != null)
{
config.router.apiBuilder(javalinEndpointGroup);
}
routeProvider.acceptJavalinConfig(config);
}
});
@ -183,6 +204,8 @@ public class QApplicationJavalinServer
service.before((Context context) -> context.header("Content-Type", "application/json"));
service.after(QJavalinImplementation::clearQContext);
addNullResponseCharsetFixer();
////////////////////////////////////////////////
// allow a configuration-customizer to be run //
////////////////////////////////////////////////
@ -196,6 +219,58 @@ public class QApplicationJavalinServer
/***************************************************************************
** initial tests with the SimpleFileSystemDirectoryRouter would sometimes
** have a Content-Type:text/html;charset=null !
** which doesn't seem every valid (and at least it broke our unit test).
** so, if w see charset=null in contentType, replace it with the system
** default, which may not be 100% right, but has to be better than "null"...
***************************************************************************/
private void addNullResponseCharsetFixer()
{
service.after((Context context) ->
{
String contentType = context.res().getContentType();
if(contentType != null && contentType.contains("charset=null"))
{
contentType = contentType.replace("charset=null", "charset=" + Charset.defaultCharset().name());
context.res().setContentType(contentType);
}
System.out.println();
});
}
/***************************************************************************
**
***************************************************************************/
private void addRouteProvidersFromMetaData(QJavalinMetaData qJavalinMetaData) throws QException
{
if(qJavalinMetaData == null)
{
return;
}
for(JavalinRouteProviderMetaData routeProviderMetaData : CollectionUtils.nonNullList(qJavalinMetaData.getRouteProviders()))
{
if(StringUtils.hasContent(routeProviderMetaData.getProcessName()) && StringUtils.hasContent(routeProviderMetaData.getHostedPath()))
{
withAdditionalRouteProvider(new ProcessBasedRouter(routeProviderMetaData));
}
else if(StringUtils.hasContent(routeProviderMetaData.getFileSystemPath()) && StringUtils.hasContent(routeProviderMetaData.getHostedPath()))
{
withAdditionalRouteProvider(new SimpleFileSystemDirectoryRouter(routeProviderMetaData));
}
else
{
throw (new QException("Error processing route provider - does not have sufficient fields set."));
}
}
}
/***************************************************************************
**
***************************************************************************/
@ -452,6 +527,21 @@ public class QApplicationJavalinServer
/*******************************************************************************
** Fluent setter to add a single additionalRouteProvider
*******************************************************************************/
public QApplicationJavalinServer withAdditionalRouteProvider(QJavalinRouteProviderInterface additionalRouteProvider)
{
if(this.additionalRouteProviders == null)
{
this.additionalRouteProviders = new ArrayList<>();
}
this.additionalRouteProviders.add(additionalRouteProvider);
return (this);
}
/*******************************************************************************
** Getter for MILLIS_BETWEEN_HOT_SWAPS
*******************************************************************************/

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.middleware.javalin;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.config.JavalinConfig;
/*******************************************************************************
@ -42,6 +43,22 @@ public interface QJavalinRouteProviderInterface
/***************************************************************************
**
***************************************************************************/
EndpointGroup getJavalinEndpointGroup();
default EndpointGroup getJavalinEndpointGroup()
{
/////////////////////////////
// no endpoints at default //
/////////////////////////////
return (null);
}
/***************************************************************************
**
***************************************************************************/
default void acceptJavalinConfig(JavalinConfig config)
{
/////////////////////
// noop at default //
/////////////////////
}
}

View File

@ -0,0 +1,175 @@
/*
* 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.middleware.javalin.metadata;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
/*******************************************************************************
**
*******************************************************************************/
public class JavalinRouteProviderMetaData implements QMetaDataObject
{
private String hostedPath;
private String fileSystemPath;
private String processName;
private List<String> methods;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public JavalinRouteProviderMetaData()
{
}
/*******************************************************************************
** Getter for hostedPath
*******************************************************************************/
public String getHostedPath()
{
return (this.hostedPath);
}
/*******************************************************************************
** Setter for hostedPath
*******************************************************************************/
public void setHostedPath(String hostedPath)
{
this.hostedPath = hostedPath;
}
/*******************************************************************************
** Fluent setter for hostedPath
*******************************************************************************/
public JavalinRouteProviderMetaData withHostedPath(String hostedPath)
{
this.hostedPath = hostedPath;
return (this);
}
/*******************************************************************************
** Getter for fileSystemPath
*******************************************************************************/
public String getFileSystemPath()
{
return (this.fileSystemPath);
}
/*******************************************************************************
** Setter for fileSystemPath
*******************************************************************************/
public void setFileSystemPath(String fileSystemPath)
{
this.fileSystemPath = fileSystemPath;
}
/*******************************************************************************
** Fluent setter for fileSystemPath
*******************************************************************************/
public JavalinRouteProviderMetaData withFileSystemPath(String fileSystemPath)
{
this.fileSystemPath = fileSystemPath;
return (this);
}
/*******************************************************************************
** Getter for processName
*******************************************************************************/
public String getProcessName()
{
return (this.processName);
}
/*******************************************************************************
** Setter for processName
*******************************************************************************/
public void setProcessName(String processName)
{
this.processName = processName;
}
/*******************************************************************************
** Fluent setter for processName
*******************************************************************************/
public JavalinRouteProviderMetaData withProcessName(String processName)
{
this.processName = processName;
return (this);
}
/*******************************************************************************
** Getter for methods
*******************************************************************************/
public List<String> getMethods()
{
return (this.methods);
}
/*******************************************************************************
** Setter for methods
*******************************************************************************/
public void setMethods(List<String> methods)
{
this.methods = methods;
}
/*******************************************************************************
** Fluent setter for methods
*******************************************************************************/
public JavalinRouteProviderMetaData withMethods(List<String> methods)
{
this.methods = methods;
return (this);
}
}

View File

@ -193,6 +193,16 @@ public interface DownloadFileSupplementalAction
***************************************************************************/
class DownloadFileSupplementalActionOutput
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public DownloadFileSupplementalActionOutput()
{
////////////////////////////////////////////////////////////////
// sorry, but here just to get test-coverage on this class... //
////////////////////////////////////////////////////////////////
int i = 0;
}
}
}

View File

@ -0,0 +1,251 @@
/*
* 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.middleware.javalin.routeproviders;
import java.io.InputStream;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.backend.javalin.QJavalinUtils;
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
/*******************************************************************************
**
*******************************************************************************/
public class ProcessBasedRouter implements QJavalinRouteProviderInterface
{
private static final QLogger LOG = QLogger.getLogger(ProcessBasedRouter.class);
private final String hostedPath;
private final String processName;
private final List<String> methods;
private QInstance qInstance;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessBasedRouter(String hostedPath, String processName)
{
this(hostedPath, processName, null);
}
/***************************************************************************
**
***************************************************************************/
public ProcessBasedRouter(JavalinRouteProviderMetaData routeProvider)
{
this(routeProvider.getHostedPath(), routeProvider.getProcessName(), routeProvider.getMethods());
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessBasedRouter(String hostedPath, String processName, List<String> methods)
{
this.hostedPath = hostedPath;
this.processName = processName;
if(CollectionUtils.nullSafeHasContents(methods))
{
this.methods = methods;
}
else
{
this.methods = List.of("GET");
}
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setQInstance(QInstance qInstance)
{
this.qInstance = qInstance;
}
/***************************************************************************
**
***************************************************************************/
@Override
public EndpointGroup getJavalinEndpointGroup()
{
return (() ->
{
for(String method : methods)
{
switch(method.toLowerCase())
{
case "get" -> ApiBuilder.get(hostedPath, this::handleRequest);
case "post" -> ApiBuilder.post(hostedPath, this::handleRequest);
case "put" -> ApiBuilder.put(hostedPath, this::handleRequest);
case "patch" -> ApiBuilder.patch(hostedPath, this::handleRequest);
case "delete" -> ApiBuilder.delete(hostedPath, this::handleRequest);
default -> throw (new IllegalArgumentException("Unrecognized method: " + method));
}
}
});
}
/***************************************************************************
**
***************************************************************************/
private void handleRequest(Context context)
{
RunProcessInput input = new RunProcessInput();
input.setProcessName(processName);
try
{
QJavalinImplementation.setupSession(context, input);
}
catch(Exception e)
{
context.header("WWW-Authenticate", "Basic realm=\"Access to this QQQ site\"");
context.status(HttpStatus.UNAUTHORIZED);
return;
}
/*
boolean authorized = false;
String authorization = context.header("Authorization");
if(authorization != null && authorization.matches("^Basic .+"))
{
String base64Authorization = authorization.substring("Basic ".length());
String decoded = new String(Base64.getDecoder().decode(base64Authorization), StandardCharsets.UTF_8);
String[] parts = decoded.split(":", 2);
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
}
if(!authorized)
{
}
// todo - not always system-user session!!
QContext.init(this.qInstance, new QSystemUserSession());
*/
try
{
LOG.info("Running [" + processName + "] to serve [" + context.path() + "]...");
/////////////////////
// run the process //
/////////////////////
input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
input.addValue("path", context.path());
input.addValue("method", context.method());
input.addValue("pathParams", new HashMap<>(context.pathParamMap()));
input.addValue("queryParams", new HashMap<>(context.queryParamMap()));
input.addValue("formParams", new HashMap<>(context.formParamMap()));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
/////////////////
// status code //
/////////////////
Integer statusCode = runProcessOutput.getValueInteger("statusCode");
if(statusCode != null)
{
context.status(statusCode);
}
/////////////////
// headers map //
/////////////////
Serializable headers = runProcessOutput.getValue("responseHeaders");
if(headers instanceof Map headersMap)
{
for(Object key : headersMap.keySet())
{
context.header(ValueUtils.getValueAsString(key), ValueUtils.getValueAsString(headersMap.get(key)));
}
}
// todo - make the inputStream available to the process
// maybe via the callback object??? input.setCallback(new QProcessCallback() {});
// context.resultInputStream();
///////////////////
// response body //
///////////////////
Serializable response = runProcessOutput.getValue("response");
if(response instanceof String s)
{
context.result(s);
}
else if(response instanceof byte[] ba)
{
context.result(ba);
}
else if(response instanceof InputStream is)
{
context.result(is);
}
else
{
context.result(ValueUtils.getValueAsString(response));
}
}
catch(Exception e)
{
QJavalinUtils.handleException(null, context, e);
}
finally
{
QContext.clear();
}
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.middleware.javalin.routeproviders;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import io.javalin.config.JavalinConfig;
import io.javalin.http.staticfiles.Location;
import io.javalin.http.staticfiles.StaticFileConfig;
/*******************************************************************************
**
*******************************************************************************/
public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInterface
{
private final String hostedPath;
private final String fileSystemPath;
private QInstance qInstance;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public SimpleFileSystemDirectoryRouter(String hostedPath, String fileSystemPath)
{
this.hostedPath = hostedPath;
this.fileSystemPath = fileSystemPath;
}
/***************************************************************************
**
***************************************************************************/
public SimpleFileSystemDirectoryRouter(JavalinRouteProviderMetaData routeProvider)
{
this(routeProvider.getHostedPath(), routeProvider.getFileSystemPath());
}
/***************************************************************************
**
***************************************************************************/
@Override
public void setQInstance(QInstance qInstance)
{
this.qInstance = qInstance;
}
/***************************************************************************
**
***************************************************************************/
@Override
public void acceptJavalinConfig(JavalinConfig config)
{
config.staticFiles.add((StaticFileConfig userConfig) ->
{
userConfig.hostedPath = hostedPath;
userConfig.directory = fileSystemPath;
userConfig.location = Location.EXTERNAL;
});
}
}

View File

@ -22,8 +22,10 @@
package com.kingsrook.qqq.backend.javalin;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.HashMap;
@ -95,6 +97,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import org.apache.commons.io.IOUtils;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -123,6 +126,8 @@ public class TestUtils
public static final String SCREEN_0 = "screen0";
public static final String SCREEN_1 = "screen1";
public static final String STATIC_SITE_PATH = Paths.get("").toAbsolutePath() + "/static-site";
/*******************************************************************************
@ -184,10 +189,25 @@ public class TestUtils
qInstance.addProcess(defineProcessScreenThenSleep());
qInstance.addProcess(defineProcessPutsNullKeyInMap());
qInstance.addProcess(defineProcessSimpleThrow());
qInstance.addProcess(defineRouterProcess());
qInstance.addReport(definePersonsReport());
qInstance.addPossibleValueSource(definePossibleValueSourcePerson());
defineWidgets(qInstance);
List<JavalinRouteProviderMetaData> routeProviders = new ArrayList<>();
if(new File(STATIC_SITE_PATH).exists())
{
routeProviders.add(new JavalinRouteProviderMetaData()
.withHostedPath("/statically-served")
.withFileSystemPath(STATIC_SITE_PATH));
}
routeProviders.add(new JavalinRouteProviderMetaData()
.withHostedPath("/served-by-process/<pagePath>")
.withProcessName("routerProcess"));
qInstance.withSupplementalMetaData(new QJavalinMetaData().withRouteProviders(routeProviders));
qInstance.addBackend(defineMemoryBackend());
try
{
@ -206,6 +226,25 @@ public class TestUtils
/***************************************************************************
*
***************************************************************************/
private static QProcessMetaData defineRouterProcess()
{
return (new QProcessMetaData()
.withName("routerProcess")
.withStep(new QBackendStepMetaData()
.withName("step")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
String path = runBackendStepInput.getValueString("path");
runBackendStepOutput.addValue("response", "So you've asked for: " + path);
}))
));
}
/***************************************************************************
**
***************************************************************************/
@ -567,7 +606,6 @@ public class TestUtils
/*******************************************************************************
** Define an interactive version of the 'greet people' process
*******************************************************************************/
@ -587,6 +625,7 @@ public class TestUtils
}
/*******************************************************************************
** Define a process with just one step that sleeps and then throws
*******************************************************************************/

View File

@ -22,16 +22,23 @@
package com.kingsrook.qqq.middleware.javalin;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.javalin.TestUtils;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.apache.commons.io.FileUtils;
import org.json.JSONObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -48,14 +55,27 @@ class QApplicationJavalinServerTest
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws IOException
{
FileUtils.writeStringToFile(new File(TestUtils.STATIC_SITE_PATH + "/foo.html"), "Foo? Bar!", Charset.defaultCharset());
}
/*******************************************************************************
**
*******************************************************************************/
@AfterEach
void afterEach()
void afterEach() throws IOException
{
javalinServer.stop();
TestApplication.callCount = 0;
FileUtils.deleteDirectory(new File(TestUtils.STATIC_SITE_PATH));
}
@ -123,6 +143,7 @@ class QApplicationJavalinServerTest
}
/*******************************************************************************
**
*******************************************************************************/
@ -173,6 +194,42 @@ class QApplicationJavalinServerTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStaticRouter() throws Exception
{
javalinServer = new QApplicationJavalinServer(getQqqApplication())
.withServeFrontendMaterialDashboard(false)
.withPort(PORT);
javalinServer.start();
Unirest.config().setDefaultResponseEncoding("UTF-8");
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/statically-served/foo.html").asString();
assertEquals("Foo? Bar!", response.getBody());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testProcessRouter() throws Exception
{
javalinServer = new QApplicationJavalinServer(getQqqApplication())
.withServeFrontendMaterialDashboard(false)
.withPort(PORT);
javalinServer.start();
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/served-by-process/foo.html").asString();
assertEquals(200, response.getStatus());
assertEquals("So you've asked for: /served-by-process/foo.html", response.getBody());
}
/***************************************************************************
**
***************************************************************************/

View File

@ -0,0 +1,82 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.sampleapp;
import java.util.Arrays;
import com.kingsrook.qqq.backend.core.instances.ConfigFilesBasedQQQApplication;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider;
/*******************************************************************************
**
*******************************************************************************/
public class ConfigFileBasedSampleJavalinServer
{
private static final QLogger LOG = QLogger.getLogger(ConfigFileBasedSampleJavalinServer.class);
private final String path;
/*******************************************************************************
**
*******************************************************************************/
public static void main(String[] args)
{
String path = "src/main/resources/metadata";
if(args.length > 0)
{
path = args[0];
System.out.println("Using path from args [" + path + "]");
}
new ConfigFileBasedSampleJavalinServer(path).start();
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ConfigFileBasedSampleJavalinServer(String path)
{
this.path = path;
}
/*******************************************************************************
**
*******************************************************************************/
public void start()
{
try
{
new QApplicationJavalinServer(new ConfigFilesBasedQQQApplication(path)).start();
}
catch(Exception e)
{
LOG.error("Failed to start javalin server. See stack trace for details.", e);
}
}
}

View File

@ -22,8 +22,10 @@
package com.kingsrook.sampleapp;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
import com.kingsrook.qqq.middleware.javalin.routeproviders.SimpleFileSystemDirectoryRouter;
import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider;
@ -53,7 +55,9 @@ public class SampleJavalinServer
{
try
{
new QApplicationJavalinServer(new SampleMetaDataProvider()).start();
QApplicationJavalinServer javalinServer = new QApplicationJavalinServer(new SampleMetaDataProvider());
javalinServer.withAdditionalRouteProvider(new SimpleFileSystemDirectoryRouter("/static-site", "/Users/dkelkhoff/tmp/static-site"));
javalinServer.start();
}
catch(Exception e)
{

View File

@ -0,0 +1,5 @@
---
class: FilesystemBackendMetaData
version: 1.0
name: filesystem
basePath: /tmp/sample-filesystem

View File

@ -0,0 +1,4 @@
---
class: QJavalinMetaData
version: 1.0
loggerDisabled: true

View File

@ -0,0 +1,5 @@
---
class: QAuthenticationMetaData
version: 1.0
name: mock
type: MOCK

View File

@ -0,0 +1,10 @@
---
class: QAppMetaData
version: 1.0
name: people
icon:
name: person
sections:
- name: People
tables:
- person

View File

@ -0,0 +1,41 @@
---
class: QTableMetaData
version: 1.0
name: person
backendName: rdbms
primaryKeyField: id
recordLabelFormat: "%s %s"
recordLabelFields:
- firstName
- lastName
fields:
id:
type: INTEGER
isEditable: false
firstName:
type: STRING
lastName:
type: STRING
## Field(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
## Field(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false))
## Field(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false))
## Field(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name").withIsRequired(true))
## Field(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name").withIsRequired(true))
## Field(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date"))
## Field(new QFieldMetaData("email", QFieldType.STRING))
## Field(new QFieldMetaData("isEmployed", QFieldType.BOOLEAN).withBackendName("is_employed"))
## Field(new QFieldMetaData("annualSalary", QFieldType.DECIMAL).withBackendName("annual_salary").withDisplayFormat(DisplayFormat.CURRENCY))
## Field(new QFieldMetaData("daysWorked", QFieldType.INTEGER).withBackendName("days_worked").withDisplayFormat(DisplayFormat.COMMAS))
sections:
- name: identity
label: Identity
icon:
name: badge
tier: T1
fieldNames:
- id
- firstName
- lastName
## Section(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("email", "birthDate")))
## Section(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("isEmployed", "annualSalary", "daysWorked")))
## Section(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));

View File

@ -0,0 +1,10 @@
---
class: RDBMSBackendMetaData
version: 1.0
name: rdbms
vendor: ${env.RDBMS_VENDOR}
hostName: ${env.RDBMS_HOSTNAME}
port: ${env.RDBMS_PORT}
databaseName: ${env.RDBMS_DATABASE_NAME}
username: ${env.RDBMS_USERNAME}
password: ${env.RDBMS_PASSWORD}