diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml index 2b0530f7..17fdf949 100644 --- a/qqq-backend-core/pom.xml +++ b/qqq-backend-core/pom.xml @@ -129,10 +129,16 @@ 2.16.0 + + com.nimbusds + oauth2-oidc-sdk + 11.23.1 + + com.auth0 auth0 - 2.1.0 + 2.18.0 com.auth0 @@ -142,12 +148,12 @@ com.auth0 jwks-rsa - 0.22.0 + 0.22.1 io.github.cdimascio - java-dotenv - 5.2.2 + dotenv-java + 3.2.0 org.apache.velocity diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java index 9b7fd619..1b30f82e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplication.java @@ -29,7 +29,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; /******************************************************************************* ** Version of AbstractQQQApplication that assumes all meta-data is produced - ** by MetaDataProducers in a single package. + ** by MetaDataProducers in (or under) a single package. *******************************************************************************/ public abstract class AbstractMetaDataProducerBasedQQQApplication extends AbstractQQQApplication { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/ConfigFilesBasedQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/ConfigFilesBasedQQQApplication.java new file mode 100644 index 00000000..633c34d4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/ConfigFilesBasedQQQApplication.java @@ -0,0 +1,61 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.instances.loaders.MetaDataLoaderHelper; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; + + +/******************************************************************************* + ** Version of AbstractQQQApplication that assumes all meta-data is defined in + ** config files (yaml, json, etc) under a given directory path. + *******************************************************************************/ +public class ConfigFilesBasedQQQApplication extends AbstractQQQApplication +{ + private final String path; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ConfigFilesBasedQQQApplication(String path) + { + this.path = path; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QInstance defineQInstance() throws QException + { + QInstance qInstance = new QInstance(); + MetaDataLoaderHelper.processAllMetaDataFilesInDirectory(qInstance, path); + return (qInstance); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplication.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplication.java new file mode 100644 index 00000000..91577042 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplication.java @@ -0,0 +1,67 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances; + + +/******************************************************************************* + ** Version of AbstractQQQApplication that assumes all meta-data is produced + ** by MetaDataProducers in (or under) a single package (where you can pass that + ** package into the constructor, vs. the abstract base class, where you extend + ** it and override the getMetaDataPackageName method. + *******************************************************************************/ +public class MetaDataProducerBasedQQQApplication extends AbstractMetaDataProducerBasedQQQApplication +{ + private final String metaDataPackageName; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public MetaDataProducerBasedQQQApplication(String metaDataPackageName) + { + this.metaDataPackageName = metaDataPackageName; + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public MetaDataProducerBasedQQQApplication(Class aClassInMetaDataPackage) + { + this(aClassInMetaDataPackage.getPackageName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public String getMetaDataPackageName() + { + return (this.metaDataPackageName); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index eb54ff6b..6895a258 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -330,7 +330,21 @@ public class QInstanceEnricher if(table.getFields() != null) { - table.getFields().values().forEach(this::enrichField); + for(Map.Entry entry : table.getFields().entrySet()) + { + String name = entry.getKey(); + QFieldMetaData field = entry.getValue(); + + //////////////////////////////////////////////////////////////////////////// + // in case the field wasn't given a name, use its key from the fields map // + //////////////////////////////////////////////////////////////////////////// + if(!StringUtils.hasContent(field.getName())) + { + field.setName(name); + } + + enrichField(field); + } for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values()) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index c73f9b8e..8f7793ef 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -64,6 +64,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaDa import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceLambda; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; @@ -651,6 +652,8 @@ public class QInstanceValidator validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class); } + authentication.validate(qInstance, this); + runPlugins(QAuthenticationMetaData.class, authentication, qInstance); } } @@ -1406,7 +1409,7 @@ public class QInstanceValidator ////////////////////////////////////////////////// // make sure the customizer can be instantiated // ////////////////////////////////////////////////// - Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass); + Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass, codeReference); TableCustomizers tableCustomizer = TableCustomizers.forRole(roleName); if(tableCustomizer == null) @@ -1467,8 +1470,13 @@ public class QInstanceValidator /******************************************************************************* ** *******************************************************************************/ - private Object getInstanceOfCodeReference(String prefix, Class clazz) + private Object getInstanceOfCodeReference(String prefix, Class clazz, QCodeReference codeReference) { + if(codeReference instanceof QCodeReferenceLambda lambdaCodeReference) + { + return (lambdaCodeReference.getLambda()); + } + Object instance = null; try { @@ -1647,21 +1655,26 @@ public class QInstanceValidator Set usedStepNames = new HashSet<>(); if(assertCondition(CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + ".")) { - int index = 0; + int index = -1; for(QStepMetaData step : process.getStepList()) { + index++; if(assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName)) { assertCondition(!usedStepNames.contains(step.getName()), "Duplicate step name [" + step.getName() + "] in process " + processName); usedStepNames.add(step.getName()); } - index++; //////////////////////////////////////////// // validate instantiation of step classes // //////////////////////////////////////////// if(step instanceof QBackendStepMetaData backendStepMetaData) { + if(assertCondition(backendStepMetaData.getCode() != null, "Missing code for a backend step at index " + index + " in process " + processName)) + { + validateSimpleCodeReference("Process " + processName + ", backend step at index " + index + ", code reference: ", backendStepMetaData.getCode(), BackendStep.class); + } + if(backendStepMetaData.getInputMetaData() != null && CollectionUtils.nullSafeHasContents(backendStepMetaData.getInputMetaData().getFieldList())) { for(QFieldMetaData fieldMetaData : backendStepMetaData.getInputMetaData().getFieldList()) @@ -2247,7 +2260,7 @@ public class QInstanceValidator ////////////////////////////////////////////////// // make sure the customizer can be instantiated // ////////////////////////////////////////////////// - Object classInstance = getInstanceOfCodeReference(prefix, clazz); + Object classInstance = getInstanceOfCodeReference(prefix, clazz, codeReference); //////////////////////////////////////////////////////////////////////// // make sure the customizer instance can be cast to the expected type // @@ -2270,6 +2283,11 @@ public class QInstanceValidator Class clazz = null; try { + if(codeReference instanceof QCodeReferenceLambda lambdaCodeReference) + { + return (lambdaCodeReference.getLambda().getClass()); + } + clazz = Class.forName(codeReference.getName()); } catch(ClassNotFoundException e) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java new file mode 100644 index 00000000..ffd659a8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoader.java @@ -0,0 +1,510 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import com.fasterxml.jackson.core.type.TypeReference; +import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.core.utils.YamlUtils; +import org.apache.commons.io.IOUtils; +import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsInteger; +import static com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsString; + + +/******************************************************************************* + ** Abstract base class in hierarchy of classes that know how to construct & + ** populate QMetaDataObject instances, based on input streams (e.g., from files). + *******************************************************************************/ +public abstract class AbstractMetaDataLoader +{ + private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class); + + private String fileName; + + private List problems = new ArrayList<>(); + + + + /*************************************************************************** + ** + ***************************************************************************/ + public T fileToMetaDataObject(QInstance qInstance, InputStream inputStream, String fileName) throws QMetaDataLoaderException + { + this.fileName = fileName; + Map map = fileToMap(inputStream, fileName); + LoadingContext loadingContext = new LoadingContext(fileName, "/"); + return (mapToMetaDataObject(qInstance, map, loadingContext)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public abstract T mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException; + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected Map fileToMap(InputStream inputStream, String fileName) throws QMetaDataLoaderException + { + try + { + String string = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + string = StringUtils.ltrim(string); + if(fileName.toLowerCase().endsWith(".json")) + { + return JsonUtils.toObject(string, new TypeReference<>() {}); + } + else if(fileName.toLowerCase().endsWith(".yaml") || fileName.toLowerCase().endsWith(".yml")) + { + return YamlUtils.toMap(string); + } + + throw (new QMetaDataLoaderException("Unsupported file format (based on file name: " + fileName + ")")); + } + catch(IOException e) + { + throw new QMetaDataLoaderException("Error building map from file: " + fileName, e); + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + protected void reflectivelyMap(QInstance qInstance, QMetaDataObject targetObject, Map map, LoadingContext context) + { + Class targetClass = targetObject.getClass(); + Set usedFieldNames = new HashSet<>(); + + for(Method method : targetClass.getMethods()) + { + try + { + if(method.getName().startsWith("set") && method.getParameterTypes().length == 1) + { + String propertyName = StringUtils.lcFirst(method.getName().substring(3)); + + if(map.containsKey(propertyName)) + { + usedFieldNames.add(propertyName); + Class parameterType = method.getParameterTypes()[0]; + Object rawValue = map.get(propertyName); + + try + { + Object mappedValue = reflectivelyMapValue(qInstance, method, parameterType, rawValue, context.descendToProperty(propertyName)); + method.invoke(targetObject, mappedValue); + } + catch(NoValueException nve) + { + /////////////////////// + // don't call setter // + /////////////////////// + LOG.debug("at " + context + ": No value was mapped for property [" + propertyName + "] on " + targetClass.getSimpleName() + "." + method.getName() + ", raw value: [" + rawValue + "]"); + } + } + } + } + catch(Exception e) + { + addProblem(new LoadingProblem(context, "Error reflectively mapping on " + targetClass.getName() + "." + method.getName(), e)); + } + } + + ////////////////////////// + // mmm, slightly sus... // + ////////////////////////// + map.remove("class"); + map.remove("version"); + + Set unrecognizedKeys = new HashSet<>(map.keySet()); + unrecognizedKeys.removeAll(usedFieldNames); + + if(!unrecognizedKeys.isEmpty()) + { + addProblem(new LoadingProblem(context, unrecognizedKeys.size() + " Unrecognized " + StringUtils.plural(unrecognizedKeys, "property", "properties") + ": " + unrecognizedKeys)); + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + public Object reflectivelyMapValue(QInstance qInstance, Method method, Class parameterType, Object rawValue, LoadingContext context) throws Exception + { + if(rawValue instanceof String s && s.matches("^\\$\\{.+\\..+}")) + { + rawValue = new QMetaDataVariableInterpreter().interpret(s); + LOG.debug("Interpreted raw value [" + s + "] as [" + StringUtils.maskAndTruncate(ValueUtils.getValueAsString(rawValue) + "]")); + } + + if(parameterType.equals(String.class)) + { + return (getValueAsString(rawValue)); + } + else if(parameterType.equals(Integer.class)) + { + try + { + return (getValueAsInteger(rawValue)); + } + catch(Exception e) + { + addProblem(new LoadingProblem(context, "[" + rawValue + "] is not an Integer value.")); + } + } + else if(parameterType.equals(Boolean.class)) + { + if("true".equals(rawValue) || Boolean.TRUE.equals(rawValue)) + { + return (true); + } + else if("false".equals(rawValue) || Boolean.FALSE.equals(rawValue)) + { + return (false); + } + else if(rawValue == null) + { + return (null); + } + else + { + addProblem(new LoadingProblem(context, "[" + rawValue + "] is not a boolean value (must be 'true' or 'false').")); + return (null); + } + } + else if(parameterType.equals(boolean.class)) + { + if("true".equals(rawValue) || Boolean.TRUE.equals(rawValue)) + { + return (true); + } + else if("false".equals(rawValue) || Boolean.FALSE.equals(rawValue)) + { + return (false); + } + else + { + addProblem(new LoadingProblem(context, rawValue + " is not a boolean value (must be 'true' or 'false').")); + throw (new NoValueException()); + } + } + else if(parameterType.equals(List.class)) + { + Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0]; + Class actualTypeClass = Class.forName(actualTypeArgument.getTypeName()); + + if(rawValue instanceof @SuppressWarnings("rawtypes")List valueList) + { + List mappedValueList = new ArrayList<>(); + for(Object o : valueList) + { + try + { + Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o, context); + mappedValueList.add(mappedValue); + } + catch(NoValueException nve) + { + // leave off list + } + } + return (mappedValueList); + } + } + else if(parameterType.equals(Set.class)) + { + Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0]; + Class actualTypeClass = Class.forName(actualTypeArgument.getTypeName()); + + if(rawValue instanceof @SuppressWarnings("rawtypes")List valueList) + { + Set mappedValueSet = new LinkedHashSet<>(); + for(Object o : valueList) + { + try + { + Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, o, context); + mappedValueSet.add(mappedValue); + } + catch(NoValueException nve) + { + // leave off list + } + } + return (mappedValueSet); + } + } + else if(parameterType.equals(Map.class)) + { + Type keyType = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[0]; + if(!keyType.equals(String.class)) + { + addProblem(new LoadingProblem(context, "Unsupported key type for " + method + " got [" + keyType + "], expected [String]")); + throw new NoValueException(); + } + // todo make sure string + + Type actualTypeArgument = ((ParameterizedType) method.getGenericParameterTypes()[0]).getActualTypeArguments()[1]; + Class actualTypeClass = Class.forName(actualTypeArgument.getTypeName()); + + if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap) + { + Map mappedValueMap = new LinkedHashMap<>(); + for(Object o : valueMap.entrySet()) + { + try + { + @SuppressWarnings("unchecked") + Map.Entry entry = (Map.Entry) o; + Object mappedValue = reflectivelyMapValue(qInstance, null, actualTypeClass, entry.getValue(), context); + mappedValueMap.put(entry.getKey(), mappedValue); + } + catch(NoValueException nve) + { + // leave out of map + } + } + return (mappedValueMap); + } + } + else if(parameterType.isEnum()) + { + String value = getValueAsString(rawValue); + for(Object enumConstant : parameterType.getEnumConstants()) + { + if(((Enum) enumConstant).name().equals(value)) + { + return (enumConstant); + } + } + + addProblem(new LoadingProblem(context, "Unrecognized value [" + rawValue + "]. Expected one of: " + Arrays.toString(parameterType.getEnumConstants()))); + } + else if(MetaDataLoaderRegistry.hasLoaderForClass(parameterType)) + { + if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap) + { + Class> loaderClass = MetaDataLoaderRegistry.getLoaderForClass(parameterType); + AbstractMetaDataLoader loader = loaderClass.getConstructor().newInstance(); + //noinspection unchecked + return (loader.mapToMetaDataObject(qInstance, valueMap, context)); + } + } + else if(QMetaDataObject.class.isAssignableFrom(parameterType)) + { + if(rawValue instanceof @SuppressWarnings("rawtypes")Map valueMap) + { + QMetaDataObject childObject = (QMetaDataObject) parameterType.getConstructor().newInstance(); + //noinspection unchecked + reflectivelyMap(qInstance, childObject, valueMap, context); + return (childObject); + } + } + else if(parameterType.equals(Serializable.class)) + { + if(rawValue instanceof String + || rawValue instanceof Integer + || rawValue instanceof BigDecimal + || rawValue instanceof Boolean + ) + { + return rawValue; + } + } + else + { + // todo clean up this message/level + addProblem(new LoadingProblem(context, "No case for " + parameterType + " (arg to: " + method + ")")); + } + + throw new NoValueException(); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // unclear if the below is needed. if so, useful to not re-write, but is hurting test coverage, so zombie until used // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ///*************************************************************************** + // * + // ***************************************************************************/ + //protected ListOfMapOrMapOfMap getListOfMapOrMapOfMap(Map map, String key) + //{ + // if(map.containsKey(key)) + // { + // if(map.get(key) instanceof List) + // { + // return (new ListOfMapOrMapOfMap((List>) map.get(key))); + // } + // else if(map.get(key) instanceof Map) + // { + // return (new ListOfMapOrMapOfMap((Map>) map.get(key))); + // } + // else + // { + // LOG.warn("Expected list or map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName())); + // } + // } + + // return (null); + //} + + ///*************************************************************************** + // * + // ***************************************************************************/ + //protected List> getListOfMap(Map map, String key) + //{ + // if(map.containsKey(key)) + // { + // if(map.get(key) instanceof List) + // { + // return (List>) map.get(key); + // } + // else + // { + // LOG.warn("Expected list under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName())); + // } + // } + + // return (null); + //} + + ///*************************************************************************** + // * + // ***************************************************************************/ + //protected Map> getMapOfMap(Map map, String key) + //{ + // if(map.containsKey(key)) + // { + // if(map.get(key) instanceof Map) + // { + // return (Map>) map.get(key); + // } + // else + // { + // LOG.warn("Expected map under key [" + key + "] while processing [" + getClass().getSimpleName() + "] from [" + fileName + "], but found: " + (map.get(key) == null ? "null" : map.get(key).getClass().getSimpleName())); + // } + // } + + // return (null); + //} + + ///*************************************************************************** + // ** + // ***************************************************************************/ + //protected record ListOfMapOrMapOfMap(List> listOf, Map> mapOf) + //{ + // /******************************************************************************* + // ** Constructor + // ** + // *******************************************************************************/ + // public ListOfMapOrMapOfMap(List> listOf) + // { + // this(listOf, null); + // } + + // /******************************************************************************* + // ** Constructor + // ** + // *******************************************************************************/ + // public ListOfMapOrMapOfMap(Map> mapOf) + // { + // this(null, mapOf); + // } + //} + + + + /******************************************************************************* + ** Getter for fileName + ** + *******************************************************************************/ + public String getFileName() + { + return fileName; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private static class NoValueException extends Exception + { + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public NoValueException() + { + super("No value"); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public void addProblem(LoadingProblem problem) + { + problems.add(problem); + } + + + + /******************************************************************************* + ** Getter for problems + ** + *******************************************************************************/ + public List getProblems() + { + return (problems); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java new file mode 100644 index 00000000..ab89d608 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoader.java @@ -0,0 +1,120 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import com.kingsrook.qqq.backend.core.instances.loaders.implementations.GenericMetaDataLoader; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.utils.ClassPathUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.core.utils.memoization.AnyKey; +import com.kingsrook.qqq.backend.core.utils.memoization.Memoization; + + +/******************************************************************************* + ** Generic implementation of AbstractMetaDataLoader, who "detects" the class + ** of meta data object to be created, then defers to an appropriate subclass + ** to do the work. + *******************************************************************************/ +public class ClassDetectingMetaDataLoader extends AbstractMetaDataLoader +{ + private static final Memoization>> memoizedMetaDataObjectClasses = new Memoization<>(); + + + /*************************************************************************** + * + ***************************************************************************/ + public AbstractMetaDataLoader getLoaderForFile(InputStream inputStream, String fileName) throws QMetaDataLoaderException + { + Map map = fileToMap(inputStream, fileName); + return (getLoaderForMap(map)); + } + + + + /*************************************************************************** + * + ***************************************************************************/ + public AbstractMetaDataLoader getLoaderForMap(Map map) throws QMetaDataLoaderException + { + if(map.containsKey("class")) + { + String classProperty = ValueUtils.getValueAsString(map.get("class")); + try + { + if(MetaDataLoaderRegistry.hasLoaderForSimpleName(classProperty)) + { + Class> loaderClass = MetaDataLoaderRegistry.getLoaderForSimpleName(classProperty); + return (loaderClass.getConstructor().newInstance()); + } + else + { + Optional>> metaDataClasses = memoizedMetaDataObjectClasses.getResult(AnyKey.getInstance(), k -> ClassPathUtils.getClassesContainingNameAndOfType("MetaData", QMetaDataObject.class)); + if(metaDataClasses.isEmpty()) + { + throw (new QMetaDataLoaderException("Could not get list of metaDataObjects from class loader")); + } + + for(Class c : metaDataClasses.get()) + { + if(c.getSimpleName().equals(classProperty) && QMetaDataObject.class.isAssignableFrom(c)) + { + @SuppressWarnings("unchecked") + Class metaDataClass = (Class) c; + return new GenericMetaDataLoader<>(metaDataClass); + } + } + } + throw new QMetaDataLoaderException("Unexpected class [" + classProperty + "] (not a QMetaDataObject; doesn't have a registered MetaDataLoader) specified in " + getFileName()); + } + catch(QMetaDataLoaderException qmdle) + { + throw (qmdle); + } + catch(Exception e) + { + throw new QMetaDataLoaderException("Error handling class [" + classProperty + "] specified in " + getFileName(), e); + } + } + else + { + throw new QMetaDataLoaderException("Cannot detect meta-data type, because [class] attribute was not specified in file: " + getFileName()); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QMetaDataObject mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException + { + AbstractMetaDataLoader loaderForMap = getLoaderForMap(map); + return loaderForMap.mapToMetaDataObject(qInstance, map, context); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingContext.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingContext.java new file mode 100644 index 00000000..3b3b3eb1 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingContext.java @@ -0,0 +1,38 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +/******************************************************************************* + ** Record to track where loader objects are - e.g., what file they're on, + ** and at what property path within the file (e.g., helps report problems). + *******************************************************************************/ +public record LoadingContext(String fileName, String propertyPath) +{ + /*************************************************************************** + ** + ***************************************************************************/ + public LoadingContext descendToProperty(String propertyName) + { + return new LoadingContext(fileName, propertyPath + propertyName + "/"); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingProblem.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingProblem.java new file mode 100644 index 00000000..ac783697 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/LoadingProblem.java @@ -0,0 +1,49 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +/******************************************************************************* + ** record that tracks a problem that was encountered when loading files. + *******************************************************************************/ +public record LoadingProblem(LoadingContext context, String message, Exception exception) // todo Level if useful +{ + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public LoadingProblem(LoadingContext context, String message) + { + this(context, message, null); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public String toString() + { + return "at[" + context.fileName() + "][" + context.propertyPath() + "]: " + message; + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java new file mode 100644 index 00000000..54a78064 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelper.java @@ -0,0 +1,118 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.Pair; + + +/******************************************************************************* + ** class that loads a directory full of meta data files into meta data objects, + ** and then sets all of them in a QInstance. + *******************************************************************************/ +public class MetaDataLoaderHelper +{ + private static final QLogger LOG = QLogger.getLogger(MetaDataLoaderHelper.class); + + + + /*************************************************************************** + * + ***************************************************************************/ + public static void processAllMetaDataFilesInDirectory(QInstance qInstance, String path) throws QException + { + List>> loaders = new ArrayList<>(); + + File directory = new File(path); + processAllMetaDataFilesInDirectory(loaders, directory); + + // todo - some version of sorting the loaders by type or possibly a sort field within the files (or file names) + + for(Pair> pair : loaders) + { + File file = pair.getA(); + AbstractMetaDataLoader loader = pair.getB(); + try(FileInputStream fileInputStream = new FileInputStream(file)) + { + QMetaDataObject qMetaDataObject = loader.fileToMetaDataObject(qInstance, fileInputStream, file.getName()); + + if(CollectionUtils.nullSafeHasContents(loader.getProblems())) + { + loader.getProblems().forEach(System.out::println); + } + + if(qMetaDataObject instanceof TopLevelMetaDataInterface topLevelMetaData) + { + topLevelMetaData.addSelfToInstance(qInstance); + } + else + { + LOG.warn("Received a non-topLevelMetaDataObject from file: " + file.getAbsolutePath()); + } + } + catch(Exception e) + { + LOG.error("Error processing file: " + file.getAbsolutePath(), e); + } + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + private static void processAllMetaDataFilesInDirectory(List>> loaders, File directory) throws QException + { + for(File file : Objects.requireNonNullElse(directory.listFiles(), new File[0])) + { + if(file.isDirectory()) + { + processAllMetaDataFilesInDirectory(loaders, file); + } + else + { + try(FileInputStream fileInputStream = new FileInputStream(file)) + { + AbstractMetaDataLoader loader = new ClassDetectingMetaDataLoader().getLoaderForFile(fileInputStream, file.getName()); + loaders.add(Pair.of(file, loader)); + } + catch(Exception e) + { + LOG.error("Error processing file: " + file.getAbsolutePath(), e); + } + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderRegistry.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderRegistry.java new file mode 100644 index 00000000..069977fb --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderRegistry.java @@ -0,0 +1,120 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.utils.ClassPathUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class MetaDataLoaderRegistry +{ + private static final QLogger LOG = QLogger.getLogger(AbstractMetaDataLoader.class); + + private static final Map, Class>> registeredLoaders = new HashMap<>(); + private static final Map>> registeredLoadersByTargetSimpleName = new HashMap<>(); + + static + { + try + { + List> classesInPackage = ClassPathUtils.getClassesInPackage(QTableMetaDataLoader.class.getPackageName()); + for(Class possibleLoaderClass : classesInPackage) + { + try + { + Type superClass = possibleLoaderClass.getGenericSuperclass(); + if(superClass.getTypeName().startsWith(AbstractMetaDataLoader.class.getName() + "<")) + { + Type actualTypeArgument = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + if(actualTypeArgument instanceof Class) + { + //noinspection unchecked + Class> loaderClass = (Class>) 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> getLoaderForClass(Class metaDataClass) + { + return registeredLoaders.get(metaDataClass); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static boolean hasLoaderForSimpleName(String targetSimpleName) + { + return registeredLoadersByTargetSimpleName.containsKey(targetSimpleName); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Class> getLoaderForSimpleName(String targetSimpleName) + { + return registeredLoadersByTargetSimpleName.get(targetSimpleName); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java new file mode 100644 index 00000000..24a3885d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/QMetaDataLoaderException.java @@ -0,0 +1,50 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QMetaDataLoaderException extends Exception +{ + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QMetaDataLoaderException(String message) + { + super(message); + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public QMetaDataLoaderException(String message, Throwable cause) + { + super(message, cause); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoader.java new file mode 100644 index 00000000..9e5c5ce5 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoader.java @@ -0,0 +1,71 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders.implementations; + + +import java.util.Map; +import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader; +import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext; +import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class GenericMetaDataLoader extends AbstractMetaDataLoader +{ + private final Class metaDataClass; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public GenericMetaDataLoader(Class metaDataClass) + { + this.metaDataClass = metaDataClass; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public T mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException + { + try + { + T object = metaDataClass.getConstructor().newInstance(); + reflectivelyMap(qInstance, object, map, context); + return (object); + } + catch(Exception e) + { + throw (new QMetaDataLoaderException("Error loading metaData object of type " + metaDataClass.getSimpleName(), e)); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QStepDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QStepDataLoader.java new file mode 100644 index 00000000..6333389b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QStepDataLoader.java @@ -0,0 +1,85 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders.implementations; + + +import java.util.Map; +import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader; +import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext; +import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QStepDataLoader extends AbstractMetaDataLoader +{ + private static final QLogger LOG = QLogger.getLogger(QStepDataLoader.class); + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QStepMetaData mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException + { + String stepType = ValueUtils.getValueAsString(map.get("stepType")); + + if(!StringUtils.hasContent(stepType)) + { + throw (new QMetaDataLoaderException("stepType was not specified for process step")); + } + + QStepMetaData step; + if("backend".equalsIgnoreCase(stepType)) + { + step = new QBackendStepMetaData(); + reflectivelyMap(qInstance, step, map, context); + } + else if("frontend".equalsIgnoreCase(stepType)) + { + step = new QFrontendStepMetaData(); + reflectivelyMap(qInstance, step, map, context); + } + // todo - we have custom factory methods for this, so, maybe needs all custom loader? + // else if("stateMachine".equalsIgnoreCase(stepType)) + // { + // step = new QStateMachineStep(); + // reflectivelyMap(qInstance, step, map, context); + // } + else + { + throw (new QMetaDataLoaderException("Unsupported step stepType: " + stepType)); + } + + return (step); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java new file mode 100644 index 00000000..c5a8d788 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/QTableMetaDataLoader.java @@ -0,0 +1,58 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders.implementations; + + +import java.util.Map; +import com.kingsrook.qqq.backend.core.instances.loaders.AbstractMetaDataLoader; +import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext; +import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QTableMetaDataLoader extends AbstractMetaDataLoader +{ + private static final QLogger LOG = QLogger.getLogger(QTableMetaDataLoader.class); + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QTableMetaData mapToMetaDataObject(QInstance qInstance, Map map, LoadingContext context) throws QMetaDataLoaderException + { + QTableMetaData table = new QTableMetaData(); + + reflectivelyMap(qInstance, table, map, context); + + // todo - handle QTableBackendDetails, based on backend's type + + return (table); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/QProcessPayload.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/QProcessPayload.java new file mode 100644 index 00000000..aad533bb --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/QProcessPayload.java @@ -0,0 +1,200 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.actions.processes; + + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntityField; +import com.kingsrook.qqq.backend.core.utils.ListingHash; +import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils; + + +/******************************************************************************* + ** base-class for bean-like classes to represent the fields of a process. + ** similar in spirit to QRecordEntity, but for processes. + *******************************************************************************/ +public class QProcessPayload +{ + private static final QLogger LOG = QLogger.getLogger(QProcessPayload.class); + + private static final ListingHash, QRecordEntityField> fieldMapping = new ListingHash<>(); + + + + /******************************************************************************* + ** Build an entity of this QRecord type from a QRecord + ** + *******************************************************************************/ + public static T fromProcessState(Class c, ProcessState processState) throws QException + { + try + { + T entity = c.getConstructor().newInstance(); + entity.populateFromProcessState(processState); + return (entity); + } + catch(Exception e) + { + throw (new QException("Error building process payload from state.", e)); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + protected void populateFromProcessState(ProcessState processState) + { + try + { + List fieldList = getFieldList(this.getClass()); + // originalRecordValues = new HashMap<>(); + + for(QRecordEntityField qRecordEntityField : fieldList) + { + Serializable value = processState.getValues().get(qRecordEntityField.getFieldName()); + Object typedValue = qRecordEntityField.convertValueType(value); + qRecordEntityField.getSetter().invoke(this, typedValue); + // originalRecordValues.put(qRecordEntityField.getFieldName(), value); + } + + // for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass())) + // { + // List associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name()); + // if(associatedRecords == null) + // { + // qRecordEntityAssociation.getSetter().invoke(this, (Object) null); + // } + // else + // { + // List associatedEntityList = new ArrayList<>(); + // for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords)) + // { + // associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord)); + // } + // qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList); + // } + // } + + // for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass())) + // { + // List associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name()); + // if(associatedRecords == null) + // { + // qRecordEntityAssociation.getSetter().invoke(this, (Object) null); + // } + // else + // { + // List associatedEntityList = new ArrayList<>(); + // for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords)) + // { + // associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord)); + // } + // qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList); + // } + // } + } + catch(Exception e) + { + throw (new QRuntimeException("Error building process payload from process state.", e)); + } + } + + + + /******************************************************************************* + ** Copy the values from this payload into the given process state. + ** ALL fields in the entity will be set in the process state. + ** + *******************************************************************************/ + public void toProcessState(ProcessState processState) throws QRuntimeException + { + try + { + for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass())) + { + processState.getValues().put(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this)); + } + } + catch(Exception e) + { + throw (new QRuntimeException("Error populating process state from process payload.", e)); + } + } + + + + /*************************************************************************** + * + ***************************************************************************/ + public static Set> allowedFieldTypes() + { + HashSet> classes = new HashSet<>(ReflectiveBeanLikeClassUtils.defaultAllowedTypes()); + classes.add(Map.class); + classes.add(List.class); + return (classes); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static List getFieldList(Class c) + { + if(!fieldMapping.containsKey(c)) + { + List fieldList = new ArrayList<>(); + for(Method possibleGetter : c.getMethods()) + { + if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, false, allowedFieldTypes())) + { + Optional setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter); + + if(setter.isPresent()) + { + String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter); + fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), null)); + } + else + { + LOG.debug("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter."); + } + } + } + fieldMapping.put(c, fieldList); + } + return (fieldMapping.get(c)); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java index ca066eb3..dad1a92d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepInput.java @@ -628,4 +628,15 @@ public class RunBackendStepInput extends AbstractActionInput { return (QContext.getQInstance().getProcess(getProcessName())); } + + + + /*************************************************************************** + ** return a QProcessPayload subclass instance, with values populated from + ** the current process state. + ***************************************************************************/ + public T getProcessPayload(Class payloadClass) throws QException + { + return QProcessPayload.fromProcessState(payloadClass, getProcessState()); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java index ba6b87c5..5f212788 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunBackendStepOutput.java @@ -445,4 +445,14 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial this.processState.setProcessMetaDataAdjustment(processMetaDataAdjustment); } + + + /*************************************************************************** + ** Update the process state with values from the input processPayload + ** subclass instance. + ***************************************************************************/ + public void setProcessPayload(QProcessPayload processPayload) + { + processPayload.toProcessState(getProcessState()); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java index dd5e7c8f..b5a17f84 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterCriteria.java @@ -33,6 +33,7 @@ import java.util.Set; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -42,7 +43,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; * *******************************************************************************/ @JsonDeserialize(using = QFilterCriteriaDeserializer.class) -public class QFilterCriteria implements Serializable, Cloneable +public class QFilterCriteria implements Serializable, Cloneable, QMetaDataObject { private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java index 52eca98c..24a2c3f9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QFilterOrderBy.java @@ -23,13 +23,14 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; /******************************************************************************* ** Bean representing an element of a query order-by clause. ** *******************************************************************************/ -public class QFilterOrderBy implements Serializable, Cloneable +public class QFilterOrderBy implements Serializable, Cloneable, QMetaDataObject { private String fieldName; private boolean isAscending = true; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java index 471bdcea..95137359 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java @@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression; import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.FilterVariableExpression; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -45,7 +46,7 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils; * Full "filter" for a query - a list of criteria and order-bys * *******************************************************************************/ -public class QQueryFilter implements Serializable, Cloneable +public class QQueryFilter implements Serializable, Cloneable, QMetaDataObject { private static final QLogger LOG = QLogger.getLogger(QQueryFilter.class); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java index d18a9615..f681aa58 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java @@ -49,6 +49,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ListingHash; import com.kingsrook.qqq.backend.core.utils.ObjectUtils; +import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -325,13 +326,13 @@ public abstract class QRecordEntity List fieldList = new ArrayList<>(); for(Method possibleGetter : c.getMethods()) { - if(isGetter(possibleGetter)) + if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, true)) { - Optional setter = getSetterForGetter(c, possibleGetter); + Optional setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter); if(setter.isPresent()) { - String fieldName = getFieldNameFromGetter(possibleGetter); + String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter); Optional fieldAnnotation = getQFieldAnnotation(c, fieldName); if(fieldAnnotation.isPresent()) @@ -378,19 +379,19 @@ public abstract class QRecordEntity List associationList = new ArrayList<>(); for(Method possibleGetter : c.getMethods()) { - if(isGetter(possibleGetter)) + if(ReflectiveBeanLikeClassUtils.isGetter(possibleGetter, true)) { - Optional setter = getSetterForGetter(c, possibleGetter); + Optional setter = ReflectiveBeanLikeClassUtils.getSetterForGetter(c, possibleGetter); if(setter.isPresent()) { - String fieldName = getFieldNameFromGetter(possibleGetter); + String fieldName = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(possibleGetter); Optional associationAnnotation = getQAssociationAnnotation(c, fieldName); if(associationAnnotation.isPresent()) { @SuppressWarnings("unchecked") - Class listTypeParam = (Class) getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType()); + Class listTypeParam = (Class) ReflectiveBeanLikeClassUtils.getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType()); associationList.add(new QRecordEntityAssociation(fieldName, possibleGetter, setter.get(), listTypeParam, associationAnnotation.orElse(null))); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java index a9999b25..91293930 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityField.java @@ -28,6 +28,7 @@ import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; +import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -170,6 +171,11 @@ public class QRecordEntityField { return (ValueUtils.getValueAsByteArray(value)); } + + if(type.equals(Map.class)) + { + return (ValueUtils.getValueAsMap(value)); + } } catch(Exception e) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java index 9ae00fbd..e8ecedf8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java @@ -28,6 +28,7 @@ package com.kingsrook.qqq.backend.core.model.metadata; *******************************************************************************/ public enum QAuthenticationType { + OAUTH2("OAuth2"), AUTH_0("auth0"), TABLE_BASED("tableBased"), FULLY_ANONYMOUS("fullyAnonymous"), diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 8e4ce83f..7b1fe014 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -1250,7 +1250,7 @@ public class QInstance { this.supplementalMetaData = new HashMap<>(); } - this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData); + this.supplementalMetaData.put(supplementalMetaData.getName(), supplementalMetaData); return (this); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java new file mode 100644 index 00000000..aeec0e2b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QMetaDataObject.java @@ -0,0 +1,34 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata; + + +import java.io.Serializable; + + +/******************************************************************************* + ** interface common among all objects that can be considered qqq meta data - + ** e.g., stored in a QInstance. + *******************************************************************************/ +public interface QMetaDataObject extends Serializable +{ +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java index 28ce1d40..c6acf7a8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QSupplementalInstanceMetaData.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.metadata; +import java.util.function.Supplier; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -30,20 +31,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; ** Base-class for instance-level meta-data defined by some supplemental module, etc, ** outside of qqq core *******************************************************************************/ -public abstract class QSupplementalInstanceMetaData implements TopLevelMetaDataInterface +public interface QSupplementalInstanceMetaData extends TopLevelMetaDataInterface { - /******************************************************************************* - ** Getter for type - *******************************************************************************/ - public abstract String getType(); - - - /******************************************************************************* ** *******************************************************************************/ - public void enrich(QTableMetaData table) + default void enrich(QTableMetaData table) { //////////////////////// // noop in base class // @@ -55,7 +49,7 @@ public abstract class QSupplementalInstanceMetaData implements TopLevelMetaDataI /******************************************************************************* ** *******************************************************************************/ - public void validate(QInstance qInstance, QInstanceValidator validator) + default void validate(QInstance qInstance, QInstanceValidator validator) { //////////////////////// // noop in base class // @@ -68,9 +62,33 @@ public abstract class QSupplementalInstanceMetaData implements TopLevelMetaDataI ** *******************************************************************************/ @Override - public void addSelfToInstance(QInstance qInstance) + default void addSelfToInstance(QInstance qInstance) { qInstance.withSupplementalMetaData(this); } + + /*************************************************************************** + ** + ***************************************************************************/ + static S of(QInstance qInstance, String name) + { + return ((S) qInstance.getSupplementalMetaData(name)); + } + + + /*************************************************************************** + ** + ***************************************************************************/ + static S ofOrWithNew(QInstance qInstance, String name, Supplier supplier) + { + S s = (S) qInstance.getSupplementalMetaData(name); + if(s == null) + { + s = supplier.get(); + s.addSelfToInstance(qInstance); + } + return (s); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java index 116bae9e..4b7cc8a3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/TopLevelMetaDataInterface.java @@ -26,7 +26,7 @@ package com.kingsrook.qqq.backend.core.model.metadata; ** Interface for meta-data classes that can be added directly (e.g, at the top ** level) to a QInstance (such as a QTableMetaData - not a QFieldMetaData). *******************************************************************************/ -public interface TopLevelMetaDataInterface extends MetaDataProducerOutput +public interface TopLevelMetaDataInterface extends MetaDataProducerOutput, QMetaDataObject { /******************************************************************************* diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java index f7cf4bfa..657c7035 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/audits/QAuditRules.java @@ -22,10 +22,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.audits; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; + + /******************************************************************************* ** *******************************************************************************/ -public class QAuditRules +public class QAuditRules implements QMetaDataObject { private AuditLevel auditLevel; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java new file mode 100644 index 00000000..13ac62b8 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java @@ -0,0 +1,320 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.authentication; + + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.OAuth2AuthenticationModule; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** Meta-data to provide details of an OAuth2 Authentication module + *******************************************************************************/ +public class OAuth2AuthenticationMetaData extends QAuthenticationMetaData +{ + private String baseUrl; + private String tokenUrl; + private String clientId; + private String scopes; + + private String userSessionTableName; + private String redirectStateTableName; + + //////////////////////////////////////////////////////////////////////////////////////// + // keep this secret, on the server - don't let it be serialized and sent to a client! // + //////////////////////////////////////////////////////////////////////////////////////// + @JsonIgnore + private String clientSecret; + + + + /******************************************************************************* + ** Default Constructor. + *******************************************************************************/ + public OAuth2AuthenticationMetaData() + { + super(); + setType(QAuthenticationType.OAUTH2); + + ////////////////////////////////////////////////////////// + // ensure this module is registered with the dispatcher // + ////////////////////////////////////////////////////////// + QAuthenticationModuleDispatcher.registerModule(QAuthenticationType.OAUTH2.getName(), OAuth2AuthenticationModule.class.getName()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void validate(QInstance qInstance, QInstanceValidator qInstanceValidator) + { + super.validate(qInstance, qInstanceValidator); + + String prefix = "OAuth2AuthenticationMetaData (named '" + getName() + "'): "; + + qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), prefix + "baseUrl must be set"); + qInstanceValidator.assertCondition(StringUtils.hasContent(clientId), prefix + "clientId must be set"); + qInstanceValidator.assertCondition(StringUtils.hasContent(clientSecret), prefix + "clientSecret must be set"); + qInstanceValidator.assertCondition(StringUtils.hasContent(scopes), prefix + "scopes must be set"); + + if(qInstanceValidator.assertCondition(StringUtils.hasContent(userSessionTableName), prefix + "userSessionTableName must be set")) + { + qInstanceValidator.assertCondition(qInstance.getTable(userSessionTableName) != null, prefix + "userSessionTableName ('" + userSessionTableName + "') was not found in the instance"); + } + + if(qInstanceValidator.assertCondition(StringUtils.hasContent(redirectStateTableName), prefix + "redirectStateTableName must be set")) + { + qInstanceValidator.assertCondition(qInstance.getTable(redirectStateTableName) != null, prefix + "redirectStateTableName ('" + redirectStateTableName + "') was not found in the instance"); + } + } + + + + /******************************************************************************* + ** Fluent setter, override to help fluent flows + *******************************************************************************/ + public OAuth2AuthenticationMetaData withBaseUrl(String baseUrl) + { + setBaseUrl(baseUrl); + return this; + } + + + + /******************************************************************************* + ** Getter for baseUrl + ** + *******************************************************************************/ + public String getBaseUrl() + { + return baseUrl; + } + + + + /******************************************************************************* + ** Setter for baseUrl + ** + *******************************************************************************/ + public void setBaseUrl(String baseUrl) + { + this.baseUrl = baseUrl; + } + + + + /******************************************************************************* + ** Fluent setter, override to help fluent flows + *******************************************************************************/ + public OAuth2AuthenticationMetaData withClientId(String clientId) + { + setClientId(clientId); + return this; + } + + + + /******************************************************************************* + ** Getter for clientId + ** + *******************************************************************************/ + public String getClientId() + { + return clientId; + } + + + + /******************************************************************************* + ** Setter for clientId + ** + *******************************************************************************/ + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + + + /******************************************************************************* + ** Fluent setter, override to help fluent flows + *******************************************************************************/ + public OAuth2AuthenticationMetaData withClientSecret(String clientSecret) + { + setClientSecret(clientSecret); + return this; + } + + + + /******************************************************************************* + ** Getter for clientSecret + ** + *******************************************************************************/ + public String getClientSecret() + { + return clientSecret; + } + + + + /******************************************************************************* + ** Setter for clientSecret + ** + *******************************************************************************/ + public void setClientSecret(String clientSecret) + { + this.clientSecret = clientSecret; + } + + + + /******************************************************************************* + ** Getter for tokenUrl + *******************************************************************************/ + public String getTokenUrl() + { + return (this.tokenUrl); + } + + + + /******************************************************************************* + ** Setter for tokenUrl + *******************************************************************************/ + public void setTokenUrl(String tokenUrl) + { + this.tokenUrl = tokenUrl; + } + + + + /******************************************************************************* + ** Fluent setter for tokenUrl + *******************************************************************************/ + public OAuth2AuthenticationMetaData withTokenUrl(String tokenUrl) + { + this.tokenUrl = tokenUrl; + return (this); + } + + + + /******************************************************************************* + ** Getter for userSessionTableName + *******************************************************************************/ + public String getUserSessionTableName() + { + return (this.userSessionTableName); + } + + + + /******************************************************************************* + ** Setter for userSessionTableName + *******************************************************************************/ + public void setUserSessionTableName(String userSessionTableName) + { + this.userSessionTableName = userSessionTableName; + } + + + + /******************************************************************************* + ** Fluent setter for userSessionTableName + *******************************************************************************/ + public OAuth2AuthenticationMetaData withUserSessionTableName(String userSessionTableName) + { + this.userSessionTableName = userSessionTableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for redirectStateTableName + *******************************************************************************/ + public String getRedirectStateTableName() + { + return (this.redirectStateTableName); + } + + + + /******************************************************************************* + ** Setter for redirectStateTableName + *******************************************************************************/ + public void setRedirectStateTableName(String redirectStateTableName) + { + this.redirectStateTableName = redirectStateTableName; + } + + + + /******************************************************************************* + ** Fluent setter for redirectStateTableName + *******************************************************************************/ + public OAuth2AuthenticationMetaData withRedirectStateTableName(String redirectStateTableName) + { + this.redirectStateTableName = redirectStateTableName; + return (this); + } + + + /******************************************************************************* + ** Getter for scopes + *******************************************************************************/ + public String getScopes() + { + return (this.scopes); + } + + + + /******************************************************************************* + ** Setter for scopes + *******************************************************************************/ + public void setScopes(String scopes) + { + this.scopes = scopes; + } + + + + /******************************************************************************* + ** Fluent setter for scopes + *******************************************************************************/ + public OAuth2AuthenticationMetaData withScopes(String scopes) + { + this.scopes = scopes; + return (this); + } + + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java index c300e7fa..33a17a3b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.authentication; import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonFilter; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; @@ -225,4 +226,15 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface return (this); } + + + /*************************************************************************** + ** + ***************************************************************************/ + public void validate(QInstance qInstance, QInstanceValidator qInstanceValidator) + { + ////////////////// + // noop at base // + ////////////////// + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java index 5beba2ea..1f154390 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/code/QCodeReference.java @@ -23,13 +23,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.code; import java.io.Serializable; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; /******************************************************************************* ** Pointer to code to be ran by the qqq framework, e.g., for custom behavior - ** maybe process steps, maybe customization to a table, etc. *******************************************************************************/ -public class QCodeReference implements Serializable, Cloneable +public class QCodeReference implements Serializable, Cloneable, QMetaDataObject { private String name; private QCodeType codeType; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index 0bbbb078..ce59465a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -40,11 +40,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole; import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ReflectiveBeanLikeClassUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -54,7 +56,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; ** Meta-data to represent a single field in a table. ** *******************************************************************************/ -public class QFieldMetaData implements Cloneable +public class QFieldMetaData implements Cloneable, QMetaDataObject { private static final QLogger LOG = QLogger.getLogger(QFieldMetaData.class); @@ -187,7 +189,7 @@ public class QFieldMetaData implements Cloneable { try { - this.name = QRecordEntity.getFieldNameFromGetter(getter); + this.name = ReflectiveBeanLikeClassUtils.getFieldNameFromGetter(getter); this.type = QFieldType.fromClass(getter.getReturnType()); @SuppressWarnings("unchecked") diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/help/QHelpContent.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/help/QHelpContent.java index f7bb173d..c99b9d1f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/help/QHelpContent.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/help/QHelpContent.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.help; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; /******************************************************************************* @@ -41,7 +42,7 @@ import java.util.Set; ** May be dynamically added to meta-data via (non-meta-) data - see ** HelpContentMetaDataProvider and QInstanceHelpContentManager *******************************************************************************/ -public class QHelpContent +public class QHelpContent implements QMetaDataObject { private String content; private HelpFormat format; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java index 012ccc6c..b713009b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppChildMetaData.java @@ -22,11 +22,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; + + /******************************************************************************* ** Interface shared by meta-data objects which can be placed into an App. ** e.g., Tables, Processes, and Apps themselves (since they can be nested) *******************************************************************************/ -public interface QAppChildMetaData +public interface QAppChildMetaData extends QMetaDataObject { /******************************************************************************* ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java index ed311341..5dc89794 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java @@ -24,12 +24,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; /******************************************************************************* ** A section of apps/tables/processes - a logical grouping. *******************************************************************************/ -public class QAppSection implements Cloneable +public class QAppSection implements Cloneable, QMetaDataObject { private String name; private String label; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java index 71c26660..3d98ee63 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QIcon.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; + + /******************************************************************************* ** Icon to show associated with an App, Table, Process, etc. ** @@ -31,7 +34,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout; ** Future may allow something like a "namespace", and/or multiple icons for ** use in different frontends, etc. *******************************************************************************/ -public class QIcon implements Cloneable +public class QIcon implements Cloneable, QMetaDataObject { private String name; private String path; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java index 07dc0d55..d94b1e8e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java @@ -22,13 +22,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.permissions; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; /******************************************************************************* ** *******************************************************************************/ -public class QPermissionRules implements Cloneable +public class QPermissionRules implements Cloneable, QMetaDataObject { private PermissionLevel level; private DenyBehavior denyBehavior; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java index aa5a1f6e..35b8316c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFrontendComponentMetaData.java @@ -25,12 +25,13 @@ package com.kingsrook.qqq.backend.core.model.metadata.processes; import java.io.Serializable; import java.util.HashMap; import java.util.Map; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; /******************************************************************************* ** Definition of a UI component in a frontend process steps. *******************************************************************************/ -public class QFrontendComponentMetaData +public class QFrontendComponentMetaData implements QMetaDataObject { private QComponentType type; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java index 21112267..baaa16ae 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java @@ -327,12 +327,23 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi /******************************************************************************* - ** Setter for stepList + ** Setter for stepList - note - calling this method ALSO overwrites the steps map! ** *******************************************************************************/ public void setStepList(List stepList) { - this.stepList = stepList; + if(stepList == null) + { + this.stepList = null; + this.steps = null; + } + else + { + this.stepList = new ArrayList<>(); + this.steps = new HashMap<>(); + } + + withStepList(stepList); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java index dd370142..6f47893e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStateMachineStep.java @@ -185,4 +185,25 @@ public class QStateMachineStep extends QStepMetaData return (rs); } + + /******************************************************************************* + ** Setter for subSteps + *******************************************************************************/ + public void setSubSteps(List subSteps) + { + this.subSteps = subSteps; + } + + + + /******************************************************************************* + ** Fluent setter for subSteps + *******************************************************************************/ + public QStateMachineStep withSubSteps(List subSteps) + { + this.subSteps = subSteps; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStepMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStepMetaData.java index d2d12545..52e7c624 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStepMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QStepMetaData.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.serialization.QStepMetaDataDeserializer; @@ -37,7 +38,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.serialization.QStepMetaData ** *******************************************************************************/ @JsonDeserialize(using = QStepMetaDataDeserializer.class) -public abstract class QStepMetaData +public abstract class QStepMetaData implements QMetaDataObject { private String name; private String label; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QScheduleMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QScheduleMetaData.java index d474a013..efcbe2b6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QScheduleMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/scheduleing/QScheduleMetaData.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.scheduleing; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -35,7 +36,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; ** same moment. ** *******************************************************************************/ -public class QScheduleMetaData +public class QScheduleMetaData implements QMetaDataObject { private String schedulerName; private String description; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java index 5ccd956e..2085d30e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/Association.java @@ -22,11 +22,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; + + /******************************************************************************* ** definition of a qqq table that is "associated" with another table, e.g., ** managed along with it - such as child-records under a parent record. *******************************************************************************/ -public class Association +public class Association implements QMetaDataObject { private String name; private String associatedTableName; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java index 157d01db..df2fbe0a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QFieldSection.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; import com.kingsrook.qqq.backend.core.instances.QInstanceHelpContentManager; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole; import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; @@ -36,7 +37,7 @@ import com.kingsrook.qqq.backend.core.utils.collections.MutableList; ** A section of fields - a logical grouping. ** TODO - this class should be named QTableSection! *******************************************************************************/ -public class QFieldSection +public class QFieldSection implements QMetaDataObject { private String name; private String label; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java index a2c1204c..8be2b1f1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/UniqueKey.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -32,7 +33,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; ** Definition of a Unique Key (or "Constraint", if you wanna use fancy words) ** on a QTable. *******************************************************************************/ -public class UniqueKey +public class UniqueKey implements QMetaDataObject { private List fieldNames; private String label; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java index e9f5d69e..e63b81d3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/AutomationStatusTracking.java @@ -22,11 +22,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables.automation; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; + + /******************************************************************************* ** Table-automation meta-data to define how this table's per-record automation ** status is tracked. *******************************************************************************/ -public class AutomationStatusTracking +public class AutomationStatusTracking implements QMetaDataObject { private AutomationStatusTrackingType type; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java index 13a57662..ad87a133 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/QTableAutomationDetails.java @@ -24,13 +24,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables.automation; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData; /******************************************************************************* ** Details about how this table's record automations are set up. *******************************************************************************/ -public class QTableAutomationDetails +public class QTableAutomationDetails implements QMetaDataObject { private AutomationStatusTracking statusTracking; private String providerName; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java index b089661c..24745549 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/automation/TableAutomationAction.java @@ -25,13 +25,14 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables.automation; import java.io.Serializable; import java.util.Map; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; /******************************************************************************* ** Definition of a specific action to run against a table *******************************************************************************/ -public class TableAutomationAction +public class TableAutomationAction implements QMetaDataObject { private String name; private TriggerEvent triggerEvent; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java index 1adcc3a4..787da174 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java @@ -22,10 +22,13 @@ package com.kingsrook.qqq.backend.core.model.session; +import java.io.Serializable; + + /******************************************************************************* ** *******************************************************************************/ -public class QUser implements Cloneable +public class QUser implements Cloneable, Serializable { private String idReference; private String fullName; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java index 28db59d6..c8c0b784 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java @@ -25,15 +25,14 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import java.io.Serializable; import java.util.Map; import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.commons.lang.NotImplementedException; + /******************************************************************************* ** Interface that a QAuthenticationModule must implement. ** @@ -82,12 +81,12 @@ public interface QAuthenticationModuleInterface } - /******************************************************************************* + /*************************************************************************** ** - *******************************************************************************/ - default String createAccessToken(QAuthenticationMetaData metaData, String clientId, String clientSecret) throws AccessTokenException + ***************************************************************************/ + default String getLoginRedirectUrl(String originalUrl) throws QAuthenticationException { - throw (new NotImplementedException("The method createAccessToken() is not implemented in the class: " + this.getClass().getSimpleName())); + throw (new NotImplementedException("The method getLoginRedirectUrl() is not implemented in the authentication module: " + this.getClass().getSimpleName())); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java index 8b871ac1..25923209 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java @@ -1020,7 +1020,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // decode the accessToken and make sure it is not expired // //////////////////////////////////////////////////////////// boolean needNewToken = true; - if(accessToken != null) + if(StringUtils.hasContent(accessToken)) { DecodedJWT jwt = JWT.decode(accessToken); String payload = jwt.getPayload(); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java index 457f6bbb..519b609f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java @@ -24,9 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication.implementations; import java.util.Map; import java.util.UUID; -import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QUser; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; @@ -77,15 +75,4 @@ public class FullyAnonymousAuthenticationModule implements QAuthenticationModule return session != null; } - - - /******************************************************************************* - ** Load an instance of the appropriate state provider - ** - *******************************************************************************/ - public String createAccessToken(QAuthenticationMetaData metaData, String clientId, String clientSecret) throws AccessTokenException - { - return (TEST_ACCESS_TOKEN); - } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java index c3459e72..4f0d5da1 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication.implementations; import java.util.Map; import java.util.UUID; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; @@ -45,8 +46,13 @@ public class MockAuthenticationModule implements QAuthenticationModuleInterface ** *******************************************************************************/ @Override - public QSession createSession(QInstance qInstance, Map context) + public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException { + if("Deny".equalsIgnoreCase(context.get("accessToken"))) + { + throw (new QAuthenticationException("Access denied (per accessToken requesting as such)")); + } + QUser qUser = new QUser(); qUser.setIdReference("User:" + (System.currentTimeMillis() % USER_ID_MODULO)); qUser.setFullName("John Smith"); @@ -80,4 +86,16 @@ public class MockAuthenticationModule implements QAuthenticationModuleInterface return (true); } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public String getLoginRedirectUrl(String originalUrl) + { + return originalUrl + "?createMockSession=true"; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java new file mode 100644 index 00000000..aa644b12 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java @@ -0,0 +1,486 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.authentication.implementations; + + +import java.io.IOException; +import java.io.Serializable; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.CapturedContext; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.OAuth2AuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; +import com.kingsrook.qqq.backend.core.model.session.QUser; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.memoization.Memoization; +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.ErrorObject; +import com.nimbusds.oauth2.sdk.GeneralException; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.TokenRequest; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; +import org.json.JSONObject; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Implementation of OAuth2 authentication. + *******************************************************************************/ +public class OAuth2AuthenticationModule implements QAuthenticationModuleInterface +{ + private static final QLogger LOG = QLogger.getLogger(OAuth2AuthenticationModule.class); + + private static boolean mayMemoize = true; + + private static final Memoization getAccessTokenFromSessionUUIDMemoization = new Memoization() + .withTimeout(Duration.of(1, ChronoUnit.MINUTES)) + .withMaxSize(1000); + + private static final Memoization oidcProviderMetadataMemoization = new Memoization() + .withMayStoreNullValues(false); + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException + { + try + { + OAuth2AuthenticationMetaData oauth2MetaData = (OAuth2AuthenticationMetaData) qInstance.getAuthentication(); + + if(context.containsKey("code") && context.containsKey("state")) + { + /////////////////////////////////////////////////////////////////////// + // handle a callback to initially auth a user for a traditional // + // (non-js) site - where the code & state params come to the backend // + /////////////////////////////////////////////////////////////////////// + AuthorizationCode code = new AuthorizationCode(context.get("code")); + + ///////////////////////////////////////// + // verify the state in our state table // + ///////////////////////////////////////// + AtomicReference redirectUri = new AtomicReference<>(null); + QContext.withTemporaryContext(new CapturedContext(qInstance, new QSystemUserSession()), () -> + { + QRecord redirectStateRecord = GetAction.execute(oauth2MetaData.getRedirectStateTableName(), Map.of("state", context.get("state"))); + if(redirectStateRecord == null) + { + throw (new QAuthenticationException("State not found")); + } + redirectUri.set(redirectStateRecord.getValueString("redirectUri")); + }); + + URI redirectURI = new URI(redirectUri.get()); + ClientSecretBasic clientSecretBasic = new ClientSecretBasic(new ClientID(oauth2MetaData.getClientId()), new Secret(oauth2MetaData.getClientSecret())); + AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, redirectURI); + + URI tokenEndpoint = getOIDCProviderMetadata(oauth2MetaData).getTokenEndpointURI(); + Scope scope = new Scope(oauth2MetaData.getScopes()); + TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientSecretBasic, codeGrant, scope); + + return createSessionFromTokenRequest(tokenRequest); + } + else if(context.containsKey("code") && context.containsKey("redirectUri") && context.containsKey("codeVerifier")) + { + //////////////////////////////////////////////////////////////////////////////// + // handle a call down to this backend code to initially auth a user for an // + // SPA that received a code (where the javascript generated the codeVerifier) // + //////////////////////////////////////////////////////////////////////////////// + AuthorizationCode code = new AuthorizationCode(context.get("code")); + URI callback = new URI(context.get("redirectUri")); + CodeVerifier codeVerifier = new CodeVerifier(context.get("codeVerifier")); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, codeVerifier); + + ClientID clientID = new ClientID(oauth2MetaData.getClientId()); + Secret clientSecret = new Secret(oauth2MetaData.getClientSecret()); + ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret); + + URI tokenEndpoint = getOIDCProviderMetadata(oauth2MetaData).getTokenEndpointURI(); + Scope scope = new Scope(oauth2MetaData.getScopes()); + TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, scope); + + return createSessionFromTokenRequest(tokenRequest); + } + else if(context.containsKey("sessionUUID") || context.containsKey("sessionId") || context.containsKey("uuid")) + { + ////////////////////////////////////////////////////////////////////// + // handle a "normal" request, where we aren't opening a new session // + // per-se, but instead are looking for one in our userSession table // + ////////////////////////////////////////////////////////////////////// + String uuid = Objects.requireNonNullElseGet(context.get("sessionUUID"), () -> + Objects.requireNonNullElseGet(context.get("sessionId"), () -> + context.get("uuid"))); + + String accessToken = getAccessTokenFromSessionUUID(uuid); + QSession session = createSessionFromToken(accessToken); + session.setUuid(uuid); + + ////////////////////////////////////////////////////////////////// + // todo - do we need to validate its age or ping the provider?? // + ////////////////////////////////////////////////////////////////// + + return (session); + } + else + { + String message = "Did not receive recognized values in context for creating session"; + LOG.warn(message, logPair("contextKeys", context.keySet())); + throw (new QAuthenticationException(message)); + } + } + catch(QAuthenticationException qae) + { + throw (qae); + } + catch(Exception e) + { + throw (new QAuthenticationException("Failed to create session (token)", e)); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private QSession createSessionFromTokenRequest(TokenRequest tokenRequest) throws ParseException, IOException, QException + { + TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send()); + + if(tokenResponse.indicatesSuccess()) + { + AccessToken accessToken = tokenResponse.toSuccessResponse().getTokens().getAccessToken(); + + //////////////////////////////////////////////////////////////////// + // todo - do we want to try to do anything with a refresh token?? // + //////////////////////////////////////////////////////////////////// + // RefreshToken refreshToken = tokenResponse.toSuccessResponse().getTokens().getRefreshToken(); + + QSession session = createSessionFromToken(accessToken.getValue()); + insertUserSession(accessToken.getValue(), session); + return (session); + } + else + { + ErrorObject errorObject = tokenResponse.toErrorResponse().getErrorObject(); + LOG.info("Token request failed", logPair("code", errorObject.getCode()), logPair("description", errorObject.getDescription())); + throw (new QAuthenticationException(errorObject.getDescription())); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean isSessionValid(QInstance instance, QSession session) + { + if(session instanceof QSystemUserSession) + { + return (true); + } + + try + { + String accessToken = getAccessTokenFromSessionUUID(session.getUuid()); + DecodedJWT jwt = JWT.decode(accessToken); + if(jwt.getExpiresAtAsInstant().isBefore(Instant.now())) + { + LOG.debug("accessToken is expired", logPair("sessionUUID", session.getUuid())); + return (false); + } + + return true; + } + catch(QAuthenticationException e) + { + return (false); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public String getLoginRedirectUrl(String originalUrl) throws QAuthenticationException + { + try + { + QInstance qInstance = QContext.getQInstance(); + OAuth2AuthenticationMetaData oauth2MetaData = (OAuth2AuthenticationMetaData) qInstance.getAuthentication(); + String authUrl = getOIDCProviderMetadata(oauth2MetaData).getAuthorizationEndpointURI().toString(); + + QTableMetaData stateTable = QContext.getQInstance().getTable(oauth2MetaData.getRedirectStateTableName()); + if(stateTable == null) + { + throw (new QAuthenticationException("The table specified as the oauthRedirectStateTableName [" + oauth2MetaData.getRedirectStateTableName() + "] is not defined in the QInstance")); + } + + /////////////////////////////////////////////////////////////////// + // generate a secure state, of either default length (32 bytes), // + // or at a size (base64 encoded) that fits in the state table // + /////////////////////////////////////////////////////////////////// + Integer stateStringLength = stateTable.getField("state").getMaxLength(); + State state = stateStringLength == null ? new State(32) : new State((stateStringLength / 4) * 3); + String stateValue = state.getValue(); + + ///////////////////////////// + // insert the state record // + ///////////////////////////// + QContext.withTemporaryContext(new CapturedContext(qInstance, new QSystemUserSession()), () -> + { + QRecord insertedState = new InsertAction().execute(new InsertInput(oauth2MetaData.getRedirectStateTableName()).withRecord(new QRecord() + .withValue("state", stateValue) + .withValue("redirectUri", originalUrl))).getRecords().get(0); + if(CollectionUtils.nullSafeHasContents(insertedState.getErrors())) + { + throw (new QAuthenticationException("Error storing redirect state: " + insertedState.getErrorsAsString())); + } + }); + + return authUrl + + "?client_id=" + URLEncoder.encode(oauth2MetaData.getClientId(), StandardCharsets.UTF_8) + + "&redirect_uri=" + URLEncoder.encode(originalUrl, StandardCharsets.UTF_8) + + "&response_type=code" + + "&scope=" + URLEncoder.encode(oauth2MetaData.getScopes(), StandardCharsets.UTF_8) + + "&state=" + URLEncoder.encode(state.getValue(), StandardCharsets.UTF_8); + } + catch(Exception e) + { + LOG.warn("Error getting login redirect url", e); + throw (new QAuthenticationException("Error getting login redirect url", e)); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private QSession createSessionFromToken(String accessToken) throws QException + { + DecodedJWT jwt = JWT.decode(accessToken); + Base64.Decoder decoder = Base64.getUrlDecoder(); + String payloadString = new String(decoder.decode(jwt.getPayload())); + JSONObject payload = new JSONObject(payloadString); + + QSession session = new QSession(); + QUser user = new QUser(); + session.setUser(user); + + user.setFullName("Unknown"); + String email = Objects.requireNonNullElseGet(payload.optString("email", null), () -> payload.optString("sub", null)); + String name = payload.optString("name", email); + + user.setIdReference(email); + user.setFullName(name); + + //////////////////////////////////////////////////////////// + // todo wip - this needs to be much better standardized w/ fe // + //////////////////////////////////////////////////////////// + session.withValueForFrontend("user", new HashMap<>(Map.of("name", name, "email", email))); + + return session; + } + + + + /******************************************************************************* + ** Insert a session as a new record into userSession table + *******************************************************************************/ + private void insertUserSession(String accessToken, QSession qSession) throws QException + { + CapturedContext capturedContext = QContext.capture(); + try + { + QContext.init(capturedContext.qInstance(), new QSystemUserSession()); + + UserSession userSession = new UserSession() + .withUuid(qSession.getUuid()) + .withUserId(qSession.getUser().getIdReference()) + .withAccessToken(accessToken); + + new InsertAction().execute(new InsertInput(UserSession.TABLE_NAME).withRecordEntity(userSession)); + } + finally + { + QContext.init(capturedContext); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QSession createAutomatedSessionForUser(QInstance qInstance, Serializable userId) throws QAuthenticationException + { + return QAuthenticationModuleInterface.super.createAutomatedSessionForUser(qInstance, userId); + } + + + + /******************************************************************************* + ** Look up access_token from session UUID + ** + *******************************************************************************/ + private String getAccessTokenFromSessionUUID(String sessionUUID) throws QAuthenticationException + { + if(mayMemoize) + { + return getAccessTokenFromSessionUUIDMemoization.getResultThrowing(sessionUUID, (String x) -> + doGetAccessTokenFromSessionUUID(sessionUUID) + ).orElse(null); + } + else + { + return (doGetAccessTokenFromSessionUUID(sessionUUID)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String doGetAccessTokenFromSessionUUID(String sessionUUID) throws QAuthenticationException + { + String accessToken = null; + QSession beforeSession = QContext.getQSession(); + + try + { + QContext.setQSession(new QSystemUserSession()); + + /////////////////////////////////////// + // query for the user session record // + /////////////////////////////////////// + QRecord userSessionRecord = new GetAction().executeForRecord(new GetInput(UserSession.TABLE_NAME) + .withUniqueKey(Map.of("uuid", sessionUUID)) + .withShouldMaskPasswords(false) + .withShouldOmitHiddenFields(false)); + + if(userSessionRecord != null) + { + accessToken = userSessionRecord.getValueString("accessToken"); + + //////////////////////////////////////////////////////////// + // decode the accessToken and make sure it is not expired // + //////////////////////////////////////////////////////////// + if(accessToken != null) + { + DecodedJWT jwt = JWT.decode(accessToken); + if(jwt.getExpiresAtAsInstant().isBefore(Instant.now())) + { + throw (new QAuthenticationException("accessToken is expired")); + } + } + } + } + catch(QAuthenticationException qae) + { + throw (qae); + } + catch(Exception e) + { + LOG.warn("Error looking up userSession by sessionUUID", e); + throw (new QAuthenticationException("Error looking up userSession by sessionUUID", e)); + } + finally + { + QContext.setQSession(beforeSession); + } + + return (accessToken); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean usesSessionIdCookie() + { + return (true); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private OIDCProviderMetadata getOIDCProviderMetadata(OAuth2AuthenticationMetaData oAuth2AuthenticationMetaData) throws GeneralException, IOException + { + return oidcProviderMetadataMemoization.getResult(oAuth2AuthenticationMetaData.getName(), (name -> + { + Issuer issuer = new Issuer(oAuth2AuthenticationMetaData.getBaseUrl()); + OIDCProviderMetadata metadata = OIDCProviderMetadata.resolve(issuer); + return (metadata); + })).orElseThrow(() -> new GeneralException("Could not resolve OIDCProviderMetadata for " + oAuth2AuthenticationMetaData.getName())); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java index 541b7d69..6c069ef9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java @@ -406,6 +406,7 @@ public class TableBasedAuthenticationModule implements QAuthenticationModuleInte qUser.setIdReference(userRecord.getValueString(metaData.getUserTableUsernameField())); QSession qSession = new QSession(); + qSession.setUuid(sessionUuid); qSession.setIdReference(sessionUuid); qSession.setUser(qUser); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/RedirectStateMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/RedirectStateMetaDataProducer.java new file mode 100644 index 00000000..c8e4f73c --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/RedirectStateMetaDataProducer.java @@ -0,0 +1,82 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; +import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; + + +/******************************************************************************* + ** Meta Data Producer for RedirectState table + *******************************************************************************/ +public class RedirectStateMetaDataProducer extends MetaDataProducer +{ + public static final String TABLE_NAME = "redirectState"; + + private final String backendName; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public RedirectStateMetaDataProducer(String backendName) + { + this.backendName = backendName; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QTableMetaData produce(QInstance qInstance) throws QException + { + QTableMetaData tableMetaData = new QTableMetaData() + .withName(TABLE_NAME) + .withBackendName(backendName) + .withRecordLabelFormat("%s") + .withRecordLabelFields("state") + .withPrimaryKeyField("id") + .withUniqueKey(new UniqueKey("state")) + .withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE)) + + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("state", QFieldType.STRING).withIsEditable(false).withMaxLength(45).withBehavior(ValueTooLongBehavior.ERROR)) + .withField(new QFieldMetaData("redirectUri", QFieldType.STRING).withIsEditable(false).withMaxLength(4096).withBehavior(ValueTooLongBehavior.ERROR)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)); + + return tableMetaData; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java index 4b90666f..bcc42f2f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ClassPathUtils.java @@ -61,6 +61,39 @@ public class ClassPathUtils + /******************************************************************************* + ** from https://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection + ** + *******************************************************************************/ + public static List> getClassesContainingNameAndOfType(String nameContains, Class type) throws IOException + { + List> classes = new ArrayList<>(); + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + + for(ClassPath.ClassInfo info : getTopLevelClasses(loader)) + { + try + { + if(info.getName().contains(nameContains)) + { + Class testClass = info.load(); + if(type.isAssignableFrom(testClass)) + { + classes.add(testClass); + } + } + } + catch(Throwable t) + { + // ignore - comes up for non-class entries, like module-info + } + } + + return (classes); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ReflectiveBeanLikeClassUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ReflectiveBeanLikeClassUtils.java new file mode 100644 index 00000000..94614769 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ReflectiveBeanLikeClassUtils.java @@ -0,0 +1,188 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.utils; + + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.data.QIgnore; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** Utilities for bean-like classes (e.g., QRecordEntity, QProcessPayload) that + ** use reflection to understand their bean-fields + *******************************************************************************/ +public class ReflectiveBeanLikeClassUtils +{ + private static final QLogger LOG = QLogger.getLogger(ReflectiveBeanLikeClassUtils.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String getFieldNameFromGetter(Method getter) + { + String nameWithoutGet = getter.getName().replaceFirst("^get", ""); + if(nameWithoutGet.length() == 1) + { + return (nameWithoutGet.toLowerCase(Locale.ROOT)); + } + return (nameWithoutGet.substring(0, 1).toLowerCase(Locale.ROOT) + nameWithoutGet.substring(1)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static boolean isGetter(Method method, boolean allowAssociations) + { + return isGetter(method, allowAssociations, defaultAllowedTypes()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static boolean isGetter(Method method, boolean allowAssociations, Collection> allowedTypes) + { + if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*")) + { + if(allowedTypes.contains(method.getReturnType()) || (allowAssociations && isSupportedAssociation(method.getReturnType(), method.getAnnotatedReturnType()))) + { + return (true); + } + else + { + if(!method.getName().equals("getClass") && method.getAnnotation(QIgnore.class) == null) + { + LOG.debug("Method [" + method.getName() + "] in [" + method.getDeclaringClass().getSimpleName() + "] looks like a getter, but its return type, [" + method.getReturnType().getSimpleName() + "], isn't supported."); + } + } + } + return (false); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional getSetterForGetter(Class c, Method getter) + { + String setterName = getter.getName().replaceFirst("^get", "set"); + for(Method method : c.getMethods()) + { + if(method.getName().equals(setterName)) + { + if(method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(getter.getReturnType())) + { + return (Optional.of(method)); + } + else + { + LOG.info("Method [" + method.getName() + "] looks like a setter for [" + getter.getName() + "], but its parameters, [" + Arrays.toString(method.getParameterTypes()) + "], don't match the getter's return type [" + getter.getReturnType() + "]"); + } + } + } + return (Optional.empty()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Collection> defaultAllowedTypes() + { + ///////////////////////////////////////////// + // note - this list has implications upon: // + // - QFieldType.fromClass // + // - QRecordEntityField.convertValueType // + ///////////////////////////////////////////// + return (Set.of(String.class, + Integer.class, + Long.class, + int.class, + Boolean.class, + boolean.class, + BigDecimal.class, + Instant.class, + LocalDate.class, + LocalTime.class, + byte[].class)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isSupportedAssociation(Class returnType, AnnotatedType annotatedType) + { + Class listTypeParam = getListTypeParam(returnType, annotatedType); + return (listTypeParam != null && QRecordEntity.class.isAssignableFrom(listTypeParam)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Class getListTypeParam(Class listType, AnnotatedType annotatedType) + { + if(listType.equals(List.class)) + { + if(annotatedType instanceof AnnotatedParameterizedType apt) + { + AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments(); + for(AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments) + { + Type type = annotatedActualTypeArgument.getType(); + if(type instanceof Class c) + { + return (c); + } + } + } + } + + return (null); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java index 313cb069..a29d9e24 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.utils; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -532,4 +533,59 @@ public class StringUtils return base + " (1)"; } } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String maskAndTruncate(String value) + { + return (maskAndTruncate(value, "** MASKED **", 6, 4)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static String maskAndTruncate(String value, String mask, int minLengthToMask, int charsToShowOnEnds) + { + if(!hasContent(value)) + { + return (""); + } + + if(value.length() < minLengthToMask || value.length() < 2 * charsToShowOnEnds) + { + return mask; + } + + if(value.length() < charsToShowOnEnds * 3) + { + return (value.substring(0, charsToShowOnEnds) + mask); + } + + return (value.substring(0, charsToShowOnEnds) + mask + value.substring(value.length() - charsToShowOnEnds)); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static String nCopies(int n, String s) + { + return (nCopiesWithGlue(n, s, "")); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static String nCopiesWithGlue(int n, String s, String glue) + { + return (StringUtils.join(glue, Collections.nCopies(n, s))); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java index 1e791abd..4bdc405c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/ValueUtils.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.utils; +import java.io.IOException; import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; @@ -38,6 +39,7 @@ import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.util.Calendar; import java.util.List; +import java.util.Map; import java.util.TimeZone; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QValueException; @@ -1012,4 +1014,37 @@ public class ValueUtils return defaultIfCannotInfer; } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public static Map getValueAsMap(Serializable value) + { + if(value == null) + { + return (null); + } + else if(value instanceof Map map) + { + return (map); + } + else if(value instanceof String string && string.startsWith("{") && string.endsWith("}")) + { + try + { + Map map = JsonUtils.toObject(string, Map.class); + return (map); + } + catch(IOException e) + { + throw new QValueException("Error parsing string to map", e); + } + } + else + { + throw new QValueException("Unrecognized object type in getValueAsMap: " + value.getClass().getSimpleName()); + } + } } diff --git a/qqq-backend-core/src/main/resources/log4j2.xml b/qqq-backend-core/src/main/resources/log4j2.xml index df03564e..06e98981 100644 --- a/qqq-backend-core/src/main/resources/log4j2.xml +++ b/qqq-backend-core/src/main/resources/log4j2.xml @@ -24,6 +24,7 @@ + diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java index d5321bcc..c854eed1 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/AbstractMetaDataProducerBasedQQQApplicationTest.java @@ -43,6 +43,9 @@ class AbstractMetaDataProducerBasedQQQApplicationTest extends BaseTest { QInstance qInstance = new TestApplication().defineQInstance(); assertEquals(1, qInstance.getTables().size()); + assertEquals("fromProducer", qInstance.getTables().get("fromProducer").getName()); + assertEquals(1, qInstance.getProcesses().size()); + assertEquals("fromProducer", qInstance.getProcesses().get("fromProducer").getName()); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplicationTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplicationTest.java new file mode 100644 index 00000000..2a22b7e2 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/MetaDataProducerBasedQQQApplicationTest.java @@ -0,0 +1,66 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances; + + +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.instances.producers.TestMetaDataProducer; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for MetaDataProducerBasedQQQApplication + *******************************************************************************/ +class MetaDataProducerBasedQQQApplicationTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws QException + { + QInstance qInstance = new MetaDataProducerBasedQQQApplication(getClass().getPackage().getName() + ".producers").defineQInstance(); + assertEquals(1, qInstance.getTables().size()); + assertEquals("fromProducer", qInstance.getTables().get("fromProducer").getName()); + assertEquals(1, qInstance.getProcesses().size()); + assertEquals("fromProducer", qInstance.getProcesses().get("fromProducer").getName()); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testConstructorThatTakeClass() throws QException + { + QInstance qInstance = new MetaDataProducerBasedQQQApplication(TestMetaDataProducer.class).defineQInstance(); + assertEquals(1, qInstance.getTables().size()); + assertEquals("fromProducer", qInstance.getTables().get("fromProducer").getName()); + assertEquals(1, qInstance.getProcesses().size()); + assertEquals("fromProducer", qInstance.getProcesses().get("fromProducer").getName()); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoaderTest.java new file mode 100644 index 00000000..fb45da10 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/AbstractMetaDataLoaderTest.java @@ -0,0 +1,136 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.nio.charset.StandardCharsets; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.instances.loaders.implementations.GenericMetaDataLoader; +import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for AbstractMetaDataLoader + *******************************************************************************/ +class AbstractMetaDataLoaderTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testVariousPropertyTypes() throws QMetaDataLoaderException + { + QProcessMetaData process = new GenericMetaDataLoader<>(QProcessMetaData.class).fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QProcessMetaData + version: 1 + name: myProcess + tableName: someTable + maxInputRecords: 1 + isHidden: true + """, StandardCharsets.UTF_8), "myProcess.yaml"); + + assertEquals("myProcess", process.getName()); + assertEquals("someTable", process.getTableName()); + assertEquals(1, process.getMaxInputRecords()); + assertTrue(process.getIsHidden()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProblems() throws QMetaDataLoaderException + { + { + QTableMetaDataLoader loader = new QTableMetaDataLoader(); + loader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QTableMetaData + version: 1.0 + name: myTable + something: foo + isHidden: hi + icon: + name: account_tree + size: big + weight: bold + fields: + id: + type: number + uniqueKeys: sure! + """, StandardCharsets.UTF_8), "myTable.yaml"); + + for(LoadingProblem problem : loader.getProblems()) + { + System.out.println(problem); + } + } + + { + GenericMetaDataLoader loader = new GenericMetaDataLoader<>(QProcessMetaData.class); + loader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QProcessMetaData + version: 1.0 + name: myProcess + maxInputRecords: many + """, StandardCharsets.UTF_8), "myProcess.yaml"); + + for(LoadingProblem problem : loader.getProblems()) + { + System.out.println(problem); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testEnvironmentValues() throws QMetaDataLoaderException + { + System.setProperty("myProcess.tableName", "someTable"); + System.setProperty("myProcess.maxInputRecords", "47"); + + GenericMetaDataLoader loader = new GenericMetaDataLoader<>(QProcessMetaData.class); + QProcessMetaData processMetaData = loader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QProcessMetaData + version: 1.0 + name: myProcess + tableName: ${prop.myProcess.tableName} + maxInputRecords: ${prop.myProcess.maxInputRecords} + """, StandardCharsets.UTF_8), "myProcess.yaml"); + + assertEquals("someTable", processMetaData.getTableName()); + assertEquals(47, processMetaData.getMaxInputRecords()); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java new file mode 100644 index 00000000..ea6cc23b --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/ClassDetectingMetaDataLoaderTest.java @@ -0,0 +1,115 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.nio.charset.StandardCharsets; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for ClassDetectingMetaDataLoader + *******************************************************************************/ +class ClassDetectingMetaDataLoaderTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBasicSuccess() throws QMetaDataLoaderException + { + QMetaDataObject qMetaDataObject = new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QTableMetaData + version: 1 + name: myTable + backendName: someBackend + """, StandardCharsets.UTF_8), "myTable.yaml"); + + assertThat(qMetaDataObject).isInstanceOf(QTableMetaData.class); + QTableMetaData qTableMetaData = (QTableMetaData) qMetaDataObject; + assertEquals("myTable", qTableMetaData.getName()); + assertEquals("someBackend", qTableMetaData.getBackendName()); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcess() throws QMetaDataLoaderException + { + QMetaDataObject qMetaDataObject = new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QProcessMetaData + version: 1 + name: myProcess + tableName: someTable + """, StandardCharsets.UTF_8), "myProcess.yaml"); + + assertThat(qMetaDataObject).isInstanceOf(QProcessMetaData.class); + QProcessMetaData qProcessMetaData = (QProcessMetaData) qMetaDataObject; + assertEquals("myProcess", qProcessMetaData.getName()); + assertEquals("someTable", qProcessMetaData.getTableName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUnknownClassFails() + { + assertThatThrownBy(() -> new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: ya whatever + version: 1 + name: myTable + """, StandardCharsets.UTF_8), "whatever.yaml")) + .isInstanceOf(QMetaDataLoaderException.class) + .hasMessageContaining("Unexpected class"); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMissingClassAttributeFails() + { + assertThatThrownBy(() -> new ClassDetectingMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + version: 1 + name: myTable + """, StandardCharsets.UTF_8), "aTable.yaml")) + .isInstanceOf(QMetaDataLoaderException.class) + .hasMessageContaining("[class] attribute was not specified"); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelperTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelperTest.java new file mode 100644 index 00000000..327e8aef --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/MetaDataLoaderHelperTest.java @@ -0,0 +1,111 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for MetaDataLoaderHelper + *******************************************************************************/ +class MetaDataLoaderHelperTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws Exception + { + Path tempDirectory = Files.createTempDirectory(getClass().getSimpleName()); + + writeFile("myTable", ".yaml", tempDirectory, """ + class: QTableMetaData + version: 1 + name: myTable + label: This is My Table + primaryKeyField: id + fields: + id: + name: id + type: INTEGER + name: + name: name + type: STRING + createDate: + name: createDate + type: DATE_TIME + """); + + writeFile("yourTable", ".yaml", tempDirectory, """ + class: QTableMetaData + version: 1 + name: yourTable + label: Someone else's table + primaryKeyField: id + fields: + id: + name: id + type: INTEGER + name: + name: name + type: STRING + """); + + QInstance qInstance = new QInstance(); + MetaDataLoaderHelper.processAllMetaDataFilesInDirectory(qInstance, tempDirectory.toFile().getAbsolutePath()); + + assertEquals(2, qInstance.getTables().size()); + + QTableMetaData myTable = qInstance.getTable("myTable"); + assertEquals("This is My Table", myTable.getLabel()); + assertEquals(3, myTable.getFields().size()); + assertEquals("id", myTable.getField("id").getName()); + assertEquals(QFieldType.INTEGER, myTable.getField("id").getType()); + + QTableMetaData yourTable = qInstance.getTable("yourTable"); + assertEquals("Someone else's table", yourTable.getLabel()); + assertEquals(2, yourTable.getFields().size()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + void writeFile(String prefix, String suffix, Path directory, String content) throws IOException + { + FileUtils.writeStringToFile(File.createTempFile(prefix, suffix, directory.toFile()), content, StandardCharsets.UTF_8); + } +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QProcessMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QProcessMetaDataLoaderTest.java new file mode 100644 index 00000000..edde858d --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QProcessMetaDataLoaderTest.java @@ -0,0 +1,103 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for loading a QProcessMetaData (doesn't need its own loader yet, + ** but is still a valuable high-level test target). + *******************************************************************************/ +class QProcessMetaDataLoaderTest extends BaseTest +{ + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testYaml() throws QMetaDataLoaderException + { + ClassDetectingMetaDataLoader metaDataLoader = new ClassDetectingMetaDataLoader(); + QProcessMetaData process = (QProcessMetaData) metaDataLoader.fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QProcessMetaData + version: 1.0 + name: myProcess + stepList: + - name: myBackendStep + stepType: backend + code: + name: com.kingsrook.test.processes.MyBackendStep + - name: myFrontendStep + stepType: frontend + components: + - type: HELP_TEXT + values: + foo: bar + - type: VIEW_FORM + viewFields: + - name: myField + type: STRING + - name: yourField + type: DATE + """, StandardCharsets.UTF_8), "myProcess.yaml"); + + CollectionUtils.nonNullList(metaDataLoader.getProblems()).forEach(System.out::println); + + assertEquals("myProcess", process.getName()); + assertEquals(2, process.getAllSteps().size()); + + QBackendStepMetaData myBackendStep = process.getBackendStep("myBackendStep"); + assertNotNull(myBackendStep, "myBackendStep should not be null"); + // todo - propagate this? assertEquals("myBackendStep", myBackendStep.getName()); + assertEquals("com.kingsrook.test.processes.MyBackendStep", myBackendStep.getCode().getName()); + + QFrontendStepMetaData myFrontendStep = process.getFrontendStep("myFrontendStep"); + assertNotNull(myFrontendStep, "myFrontendStep should not be null"); + assertEquals(2, myFrontendStep.getComponents().size()); + assertEquals(QComponentType.HELP_TEXT, myFrontendStep.getComponents().get(0).getType()); + assertEquals(Map.of("foo", "bar"), myFrontendStep.getComponents().get(0).getValues()); + assertEquals(QComponentType.VIEW_FORM, myFrontendStep.getComponents().get(1).getType()); + + assertEquals(2, myFrontendStep.getViewFields().size()); + assertEquals("myField", myFrontendStep.getViewFields().get(0).getName()); + assertEquals(QFieldType.STRING, myFrontendStep.getViewFields().get(0).getType()); + assertEquals("yourField", myFrontendStep.getViewFields().get(1).getName()); + assertEquals(QFieldType.DATE, myFrontendStep.getViewFields().get(1).getType()); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java new file mode 100644 index 00000000..cef4d89b --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/QTableMetaDataLoaderTest.java @@ -0,0 +1,202 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders; + + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Set; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.instances.loaders.implementations.QTableMetaDataLoader; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import com.kingsrook.qqq.backend.core.utils.YamlUtils; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/******************************************************************************* + ** Unit test for QTableMetaDataLoader + *******************************************************************************/ +class QTableMetaDataLoaderTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + @Disabled("Not quite yet passing - is a good goal to get to though!") + void testToYaml() throws QMetaDataLoaderException + { + QTableMetaData expectedTable = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + String expectedYaml = YamlUtils.toYaml(expectedTable); + QTableMetaData actualTable = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(expectedYaml, StandardCharsets.UTF_8), "person.yaml"); + String actualYaml = YamlUtils.toYaml(actualTable); + assertEquals(expectedYaml, actualYaml); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testYaml() throws QMetaDataLoaderException + { + QTableMetaData table = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QTableMetaData + version: 1.0 + name: myTable + icon: + name: account_tree + fields: + id: + name: id + type: INTEGER + name: + name: name + type: STRING + uniqueKeys: + - label: Name + fieldNames: + - name + associations: + - name: A1 + associatedTableName: yourTable + joinName: myTableJoinYourTable + - name: A2 + associatedTableName: theirTable + joinName: myTableJoinTheirTable + permissionRules: + level: READ_WRITE_PERMISSIONS + denyBehavior: HIDDEN + permissionBaseName: myTablePermissions + customPermissionChecker: + name: com.kingsrook.SomeChecker + codeType: JAVA + ## todo recordSecurityLocks + ## todo auditRules + ## todo backendDetails + ## todo automationDetails + sections: + - name: identity + label: Identity + icon: + name: badge + tier: T1 + fieldNames: + - id + - firstName + - lastName + customizers: + postQueryRecord: + name: com.kingsrook.SomePostQuery + codeType: JAVA + preDeleteRecord: + name: com.kingsrook.SomePreDelete + codeType: JAVA + disabledCapabilities: + - TABLE_COUNT + - QUERY_STATS + """, StandardCharsets.UTF_8), "myTable.yaml"); + + assertEquals("myTable", table.getName()); + + assertEquals(2, table.getFields().size()); + // assertEquals("id", table.getFields().get("id").getName()); + assertEquals(QFieldType.INTEGER, table.getFields().get("id").getType()); + // assertEquals("name", table.getFields().get("name").getName()); + assertEquals(QFieldType.STRING, table.getFields().get("name").getType()); + + assertNotNull(table.getIcon()); + assertEquals("account_tree", table.getIcon().getName()); + + assertEquals(1, table.getUniqueKeys().size()); + assertEquals(List.of("name"), table.getUniqueKeys().get(0).getFieldNames()); + assertEquals("Name", table.getUniqueKeys().get(0).getLabel()); + + assertEquals(2, table.getAssociations().size()); + assertEquals("A1", table.getAssociations().get(0).getName()); + assertEquals("theirTable", table.getAssociations().get(1).getAssociatedTableName()); + + assertNotNull(table.getPermissionRules()); + assertEquals(PermissionLevel.READ_WRITE_PERMISSIONS, table.getPermissionRules().getLevel()); + assertEquals(DenyBehavior.HIDDEN, table.getPermissionRules().getDenyBehavior()); + assertEquals("myTablePermissions", table.getPermissionRules().getPermissionBaseName()); + assertNotNull(table.getPermissionRules().getCustomPermissionChecker()); + assertEquals("com.kingsrook.SomeChecker", table.getPermissionRules().getCustomPermissionChecker().getName()); + assertEquals(QCodeType.JAVA, table.getPermissionRules().getCustomPermissionChecker().getCodeType()); + + assertEquals(1, table.getSections().size()); + assertEquals("identity", table.getSections().get(0).getName()); + assertEquals(Tier.T1, table.getSections().get(0).getTier()); + assertEquals(List.of("id", "firstName", "lastName"), table.getSections().get(0).getFieldNames()); + + assertEquals(2, table.getCustomizers().size()); + assertEquals("com.kingsrook.SomePostQuery", table.getCustomizers().get(TableCustomizers.POST_QUERY_RECORD.getRole()).getName()); + assertEquals("com.kingsrook.SomePreDelete", table.getCustomizers().get(TableCustomizers.PRE_DELETE_RECORD.getRole()).getName()); + + assertEquals(Set.of(Capability.TABLE_COUNT, Capability.QUERY_STATS), table.getDisabledCapabilities()); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSimpleJson() throws QMetaDataLoaderException + { + QTableMetaData table = new QTableMetaDataLoader().fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + { + "class": "QTableMetaData", + "version": "1.0", + "name": "myTable", + "fields": + { + "id": {"name": "id", "type": "INTEGER"}, + "name": {"name": "name", "type": "STRING"} + } + } + """, StandardCharsets.UTF_8), "myTable.json"); + + assertEquals("myTable", table.getName()); + assertEquals(2, table.getFields().size()); + assertEquals("id", table.getFields().get("id").getName()); + assertEquals(QFieldType.INTEGER, table.getFields().get("id").getType()); + assertEquals("name", table.getFields().get("name").getName()); + assertEquals(QFieldType.STRING, table.getFields().get("name").getType()); + } + + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoaderTest.java new file mode 100644 index 00000000..19721921 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/loaders/implementations/GenericMetaDataLoaderTest.java @@ -0,0 +1,81 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.loaders.implementations; + + +import java.nio.charset.StandardCharsets; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.instances.loaders.LoadingContext; +import com.kingsrook.qqq.backend.core.instances.loaders.QMetaDataLoaderException; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for GenericMetaDataLoader - providing coverage for AbstractMetaDataLoader. + *******************************************************************************/ +class GenericMetaDataLoaderTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcess() throws QMetaDataLoaderException + { + //////////////////////////////////////////////////////////////////////////////// + // trying to get some coverage of various types in here (for Abstract loader) // + //////////////////////////////////////////////////////////////////////////////// + QProcessMetaData process = new GenericMetaDataLoader<>(QProcessMetaData.class).fileToMetaDataObject(new QInstance(), IOUtils.toInputStream(""" + class: QProcessMetaData + version: 1 + name: myProcess + tableName: someTable + maxInputRecords: 1 + isHidden: true + """, StandardCharsets.UTF_8), "myProcess.yaml"); + + assertEquals("myProcess", process.getName()); + assertEquals("someTable", process.getTableName()); + assertEquals(1, process.getMaxInputRecords()); + assertTrue(process.getIsHidden()); + } + + + + /******************************************************************************* + ** just here for coverage of this class, as we're failing to hit it otherwise. + *******************************************************************************/ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + void testNoValueException() + { + assertThatThrownBy(() -> new GenericMetaDataLoader(QBackendMetaData.class).reflectivelyMapValue(new QInstance(), null, GenericMetaDataLoaderTest.class, "rawValue", new LoadingContext("test.yaml", "/"))); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/subpackage/TestProcessMetaDataProducer.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/subpackage/TestProcessMetaDataProducer.java new file mode 100644 index 00000000..5f7e6289 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/producers/subpackage/TestProcessMetaDataProducer.java @@ -0,0 +1,47 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.instances.producers.subpackage; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TestProcessMetaDataProducer implements MetaDataProducerInterface +{ + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QProcessMetaData produce(QInstance qInstance) throws QException + { + return new QProcessMetaData().withName("fromProducer"); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java index 9faaabb1..391c9a77 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/StringUtilsTest.java @@ -368,4 +368,76 @@ class StringUtilsTest extends BaseTest assertTrue(StringUtils.safeEqualsIgnoreCase("timothy d. chamberlain", "timothy d. chamberlain")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNCopies() + { + assertEquals("", StringUtils.nCopies(0, "a")); + assertEquals("a", StringUtils.nCopies(1, "a")); + assertEquals("aa", StringUtils.nCopies(2, "a")); + assertEquals("ab ab ab ", StringUtils.nCopies(3, "ab ")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNCopiesWithGlue() + { + assertEquals("", StringUtils.nCopiesWithGlue(0, "a", "")); + assertEquals("", StringUtils.nCopiesWithGlue(0, "a", ",")); + assertEquals("a", StringUtils.nCopiesWithGlue(1, "a", ",")); + assertEquals("aa", StringUtils.nCopiesWithGlue(2, "a", "")); + assertEquals("a,a", StringUtils.nCopiesWithGlue(2, "a", ",")); + assertEquals("ab ab ab", StringUtils.nCopiesWithGlue(3, "ab", " ")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMaskAndTruncate() + { + assertEquals("", StringUtils.maskAndTruncate(null)); + assertEquals("", StringUtils.maskAndTruncate("")); + assertEquals("** MASKED **", StringUtils.maskAndTruncate("1")); + assertEquals("** MASKED **", StringUtils.maskAndTruncate("12")); + assertEquals("** MASKED **", StringUtils.maskAndTruncate("123")); + assertEquals("** MASKED **", StringUtils.maskAndTruncate("1234")); + assertEquals("** MASKED **", StringUtils.maskAndTruncate("12345")); + assertEquals("** MASKED **", StringUtils.maskAndTruncate("123456")); + assertEquals("** MASKED **", StringUtils.maskAndTruncate("1234567")); + assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("12345678")); + assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("123456789")); + assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("1234567890")); + assertEquals("1234** MASKED **", StringUtils.maskAndTruncate("12345678901")); + assertEquals("1234** MASKED **9012", StringUtils.maskAndTruncate("123456789012")); + assertEquals("1234** MASKED **6789", StringUtils.maskAndTruncate("123456789" + StringUtils.nCopies(100, "xyz") + "123456789")); + + assertEquals("***", StringUtils.maskAndTruncate("12", "***", 3, 1)); + assertEquals("1***3", StringUtils.maskAndTruncate("123", "***", 3, 1)); + assertEquals("1***4", StringUtils.maskAndTruncate("1234", "***", 3, 1)); + assertEquals("1***5", StringUtils.maskAndTruncate("12345", "***", 3, 1)); + assertEquals("12***", StringUtils.maskAndTruncate("12345", "***", 3, 2)); + assertEquals("12***56", StringUtils.maskAndTruncate("123456", "***", 3, 2)); + + assertEquals("***", StringUtils.maskAndTruncate("12", "***", 3, 4)); + assertEquals("***", StringUtils.maskAndTruncate("123", "***", 3, 4)); + assertEquals("***", StringUtils.maskAndTruncate("1234", "***", 3, 4)); + assertEquals("***", StringUtils.maskAndTruncate("12345", "***", 3, 4)); + assertEquals("***", StringUtils.maskAndTruncate("12345", "***", 3, 4)); + assertEquals("***", StringUtils.maskAndTruncate("123456", "***", 3, 4)); + assertEquals("1234***", StringUtils.maskAndTruncate("1234567890", "***", 3, 4)); + assertEquals("1234***", StringUtils.maskAndTruncate("12345678901", "***", 3, 4)); + assertEquals("1234***9012", StringUtils.maskAndTruncate("123456789012", "***", 3, 4)); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 54502074..731b7235 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -40,6 +40,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.logging.QLogger; @@ -1289,7 +1290,7 @@ public class TestUtils /******************************************************************************* ** *******************************************************************************/ - public static QSession getMockSession() + public static QSession getMockSession() throws QAuthenticationException { MockAuthenticationModule mockAuthenticationModule = new MockAuthenticationModule(); return (mockAuthenticationModule.createSession(null, null)); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java index 2ea76133..767d2669 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/ApiInstanceMetaDataContainer.java @@ -34,7 +34,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** *******************************************************************************/ -public class ApiInstanceMetaDataContainer extends QSupplementalInstanceMetaData +public class ApiInstanceMetaDataContainer implements QSupplementalInstanceMetaData { private Map apis; @@ -71,17 +71,6 @@ public class ApiInstanceMetaDataContainer extends QSupplementalInstanceMetaData - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public String getType() - { - return (ApiSupplementType.NAME); - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 80f227d3..5ed116b1 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -206,7 +206,7 @@ public class QJavalinImplementation *******************************************************************************/ public QJavalinImplementation(QInstance qInstance) throws QInstanceValidationException { - this(qInstance, new QJavalinMetaData()); + this(qInstance, QJavalinMetaData.ofOrWithNew(qInstance)); } @@ -453,10 +453,21 @@ public class QJavalinImplementation QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication()); Map authContext = new HashMap<>(); - //? authContext.put("uuid", ValueUtils.getValueAsString(map.get("uuid"))); - authContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, ValueUtils.getValueAsString(map.get("accessToken"))); authContext.put(Auth0AuthenticationModule.DO_STORE_USER_SESSION_KEY, "true"); + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // before this code iterated the map, it had zombied uuid line, and only actually used ACCESS_TOKEN_KEY // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + //? authContext.put("uuid", ValueUtils.getValueAsString(map.get("uuid"))); + // authContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, ValueUtils.getValueAsString(map.get("accessToken"))); + ///////////////////////////////////////////////////////////////// + // todo - have the auth module declare what values it expects? // + ///////////////////////////////////////////////////////////////// + for(Map.Entry entry : map.entrySet()) + { + authContext.put(ValueUtils.getValueAsString(entry.getKey()), ValueUtils.getValueAsString(entry.getValue())); + } + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // put the qInstance into context - but no session yet (since, the whole point of this call is to manage the session!) // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -524,14 +535,24 @@ public class QJavalinImplementation try { + /////////////////////////////////////////////// + // note: duplicated in ExecutorSessionUtils // + /////////////////////////////////////////////// Map authenticationContext = new HashMap<>(); String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY); String authorizationHeaderValue = context.header("Authorization"); String apiKeyHeaderValue = context.header("x-api-key"); + String codeQueryParamValue = context.queryParam("code"); + String stateQueryParamValue = context.queryParam("state"); - if(StringUtils.hasContent(sessionIdCookieValue)) + if(StringUtils.hasContent(codeQueryParamValue) && StringUtils.hasContent(stateQueryParamValue)) + { + authenticationContext.put("code", codeQueryParamValue); + authenticationContext.put("state", stateQueryParamValue); + } + else if(StringUtils.hasContent(sessionIdCookieValue)) { /////////////////////////////////////////////////////// // sessionId - maybe used by table-based auth module // @@ -597,7 +618,7 @@ public class QJavalinImplementation ///////////////////////////////////////////////////////////////////////////////// if(authenticationModule.usesSessionIdCookie()) { - context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE); + context.cookie(SESSION_ID_COOKIE_NAME, session.getUuid(), SESSION_COOKIE_AGE); } setUserTimezoneOffsetMinutesInSession(context, session); diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java index 89f422b2..3c1ea8df 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinMetaData.java @@ -22,25 +22,66 @@ package com.kingsrook.qqq.backend.javalin; +import java.util.ArrayList; +import java.util.List; import java.util.function.Function; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData; +import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; import org.apache.logging.log4j.Level; /******************************************************************************* ** MetaData specific to a QQQ Javalin server. *******************************************************************************/ -public class QJavalinMetaData +public class QJavalinMetaData implements QSupplementalInstanceMetaData { + public static final String NAME = "javalin"; + private String uploadedFileArchiveTableName; private boolean loggerDisabled = false; + // todo - should be a code reference!! private Function logFilter; private boolean queryWithoutLimitAllowed = false; private Integer queryWithoutLimitDefault = 1000; private Level queryWithoutLimitLogLevel = Level.INFO; + private List routeProviders; + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public String getName() + { + return (NAME); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QJavalinMetaData of(QInstance qInstance) + { + return QSupplementalInstanceMetaData.of(qInstance, NAME); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QJavalinMetaData ofOrWithNew(QInstance qInstance) + { + return QSupplementalInstanceMetaData.ofOrWithNew(qInstance, NAME, QJavalinMetaData::new); + } + /******************************************************************************* @@ -241,4 +282,51 @@ public class QJavalinMetaData return (this); } + + + /******************************************************************************* + ** Getter for routeProviders + *******************************************************************************/ + public List getRouteProviders() + { + return (this.routeProviders); + } + + + + /******************************************************************************* + ** Setter for routeProviders + *******************************************************************************/ + public void setRouteProviders(List routeProviders) + { + this.routeProviders = routeProviders; + } + + + + /******************************************************************************* + ** Fluent setter for routeProviders + *******************************************************************************/ + public QJavalinMetaData withRouteProviders(List routeProviders) + { + this.routeProviders = routeProviders; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter to add 1 routeProvider + *******************************************************************************/ + public QJavalinMetaData withRouteProvider(JavalinRouteProviderMetaData routeProvider) + { + if(this.routeProviders == null) + { + this.routeProviders = new ArrayList<>(); + } + this.routeProviders.add(routeProvider); + return (this); + } + + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java index ce720a1c..b0ed8bce 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java @@ -22,6 +22,8 @@ package com.kingsrook.qqq.middleware.javalin; +import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -31,12 +33,17 @@ import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.utils.ClassPathUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; import com.kingsrook.qqq.backend.javalin.QJavalinMetaData; +import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; +import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouter; +import com.kingsrook.qqq.middleware.javalin.routeproviders.SimpleFileSystemDirectoryRouter; import com.kingsrook.qqq.middleware.javalin.specs.AbstractMiddlewareVersion; import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; import io.javalin.Javalin; +import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.Context; import org.apache.commons.lang.BooleanUtils; import org.eclipse.jetty.util.resource.Resource; @@ -70,8 +77,8 @@ public class QApplicationJavalinServer private boolean serveLegacyUnversionedMiddlewareAPI = true; private List middlewareVersionList = List.of(new MiddlewareVersionV1()); private List additionalRouteProviders = null; - private Consumer javalinConfigurationCustomizer = null; - private QJavalinMetaData javalinMetaData = null; + private Consumer javalinConfigurationCustomizer = null; + private QJavalinMetaData javalinMetaData = null; private long lastQInstanceHotSwapMillis; private long millisBetweenHotSwaps = 2500; @@ -99,6 +106,12 @@ public class QApplicationJavalinServer { QInstance qInstance = application.defineValidatedQInstance(); + QJavalinMetaData qJavalinMetaData = QJavalinMetaData.of(qInstance); + if(qJavalinMetaData != null) + { + addRouteProvidersFromMetaData(qJavalinMetaData); + } + service = Javalin.create(config -> { if(serveFrontendMaterialDashboard) @@ -117,7 +130,7 @@ public class QApplicationJavalinServer //////////////////////////////////////////////////////////////////////////////////////// try(Resource resource = Resource.newClassPathResource("/material-dashboard-overlay")) { - if(resource !=null) + if(resource != null) { config.staticFiles.add("/material-dashboard-overlay"); } @@ -173,10 +186,26 @@ public class QApplicationJavalinServer for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders)) { routeProvider.setQInstance(qInstance); - config.router.apiBuilder(routeProvider.getJavalinEndpointGroup()); + + EndpointGroup javalinEndpointGroup = routeProvider.getJavalinEndpointGroup(); + if(javalinEndpointGroup != null) + { + config.router.apiBuilder(javalinEndpointGroup); + } + + routeProvider.acceptJavalinConfig(config); } }); + ////////////////////////////////////////////////////////////////////// + // also pass the javalin service into any additionalRouteProviders, // + // in case they need additional setup, e.g., before/after handlers. // + ////////////////////////////////////////////////////////////////////// + for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders)) + { + routeProvider.acceptJavalinService(service); + } + ////////////////////////////////////////////////////////////////////////////////////// // per system property, set the server to hot-swap the q instance before all routes // ////////////////////////////////////////////////////////////////////////////////////// @@ -190,6 +219,8 @@ public class QApplicationJavalinServer service.before((Context context) -> context.header("Content-Type", "application/json")); service.after(QJavalinImplementation::clearQContext); + addNullResponseCharsetFixer(); + //////////////////////////////////////////////// // allow a configuration-customizer to be run // //////////////////////////////////////////////// @@ -203,6 +234,57 @@ public class QApplicationJavalinServer + /*************************************************************************** + ** initial tests with the SimpleFileSystemDirectoryRouter would sometimes + ** have a Content-Type:text/html;charset=null ! + ** which doesn't seem ever valid (and at least it broke our unit test). + ** so, if w see charset=null in contentType, replace it with the system + ** default, which may not be 100% right, but has to be better than "null"... + ***************************************************************************/ + private void addNullResponseCharsetFixer() + { + service.after((Context context) -> + { + String contentType = context.res().getContentType(); + if(contentType != null && contentType.contains("charset=null")) + { + contentType = contentType.replace("charset=null", "charset=" + Charset.defaultCharset().name()); + context.res().setContentType(contentType); + } + }); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void addRouteProvidersFromMetaData(QJavalinMetaData qJavalinMetaData) throws QException + { + if(qJavalinMetaData == null) + { + return; + } + + for(JavalinRouteProviderMetaData routeProviderMetaData : CollectionUtils.nonNullList(qJavalinMetaData.getRouteProviders())) + { + if(StringUtils.hasContent(routeProviderMetaData.getProcessName()) && StringUtils.hasContent(routeProviderMetaData.getHostedPath())) + { + withAdditionalRouteProvider(new ProcessBasedRouter(routeProviderMetaData)); + } + else if(StringUtils.hasContent(routeProviderMetaData.getFileSystemPath()) && StringUtils.hasContent(routeProviderMetaData.getHostedPath())) + { + withAdditionalRouteProvider(new SimpleFileSystemDirectoryRouter(routeProviderMetaData)); + } + else + { + throw (new QException("Error processing route provider - does not have sufficient fields set.")); + } + } + } + + + /*************************************************************************** ** ***************************************************************************/ @@ -459,6 +541,21 @@ public class QApplicationJavalinServer + /******************************************************************************* + ** Fluent setter to add a single additionalRouteProvider + *******************************************************************************/ + public QApplicationJavalinServer withAdditionalRouteProvider(QJavalinRouteProviderInterface additionalRouteProvider) + { + if(this.additionalRouteProviders == null) + { + this.additionalRouteProviders = new ArrayList<>(); + } + this.additionalRouteProviders.add(additionalRouteProvider); + return (this); + } + + + /******************************************************************************* ** Getter for MILLIS_BETWEEN_HOT_SWAPS *******************************************************************************/ @@ -541,6 +638,7 @@ public class QApplicationJavalinServer } + /******************************************************************************* ** Getter for javalinMetaData *******************************************************************************/ @@ -570,5 +668,4 @@ public class QApplicationJavalinServer return (this); } - } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java index dabe521c..c6dbf40b 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QJavalinRouteProviderInterface.java @@ -23,7 +23,9 @@ package com.kingsrook.qqq.middleware.javalin; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import io.javalin.Javalin; import io.javalin.apibuilder.EndpointGroup; +import io.javalin.config.JavalinConfig; /******************************************************************************* @@ -42,6 +44,37 @@ public interface QJavalinRouteProviderInterface /*************************************************************************** ** ***************************************************************************/ - EndpointGroup getJavalinEndpointGroup(); + default EndpointGroup getJavalinEndpointGroup() + { + ///////////////////////////// + // no endpoints at default // + ///////////////////////////// + return (null); + } + + + /*************************************************************************** + ** when the javalin service is being configured as part of its boot up, + ** accept the javalinConfig object, to perform whatever setup you need, + ** such as setting up routes. + ***************************************************************************/ + default void acceptJavalinConfig(JavalinConfig config) + { + ///////////////////// + // noop at default // + ///////////////////// + } + + /*************************************************************************** + ** when the javalin service is being configured as part of its boot up, + ** accept the Javalin service object, to perform whatever setup you need, + ** such as setting up before/after handlers. + ***************************************************************************/ + default void acceptJavalinService(Javalin service) + { + ///////////////////// + // noop at default // + ///////////////////// + } } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java index 7a379fd5..db6797d0 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java @@ -60,14 +60,24 @@ public class ExecutorSessionUtils try { + ///////////////////////////////////////////////// + // note: duplicated in QJavalinImplementation // + ///////////////////////////////////////////////// Map authenticationContext = new HashMap<>(); String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY); String authorizationHeaderValue = context.header("Authorization"); String apiKeyHeaderValue = context.header("x-api-key"); + String codeQueryParamValue = context.queryParam("code"); + String stateQueryParamValue = context.queryParam("state"); - if(StringUtils.hasContent(sessionIdCookieValue)) + if(StringUtils.hasContent(codeQueryParamValue) && StringUtils.hasContent(stateQueryParamValue)) + { + authenticationContext.put("code", codeQueryParamValue); + authenticationContext.put("state", stateQueryParamValue); + } + else if(StringUtils.hasContent(sessionIdCookieValue)) { /////////////////////////////////////////////////////// // sessionId - maybe used by table-based auth module // diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java new file mode 100644 index 00000000..82c09173 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/metadata/JavalinRouteProviderMetaData.java @@ -0,0 +1,209 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.metadata; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class JavalinRouteProviderMetaData implements QMetaDataObject +{ + private String hostedPath; + + private String fileSystemPath; + private String processName; + + private List methods; + + private QCodeReference routeAuthenticator; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public JavalinRouteProviderMetaData() + { + } + + + + /******************************************************************************* + ** Getter for hostedPath + *******************************************************************************/ + public String getHostedPath() + { + return (this.hostedPath); + } + + + + /******************************************************************************* + ** Setter for hostedPath + *******************************************************************************/ + public void setHostedPath(String hostedPath) + { + this.hostedPath = hostedPath; + } + + + + /******************************************************************************* + ** Fluent setter for hostedPath + *******************************************************************************/ + public JavalinRouteProviderMetaData withHostedPath(String hostedPath) + { + this.hostedPath = hostedPath; + return (this); + } + + + + /******************************************************************************* + ** Getter for fileSystemPath + *******************************************************************************/ + public String getFileSystemPath() + { + return (this.fileSystemPath); + } + + + + /******************************************************************************* + ** Setter for fileSystemPath + *******************************************************************************/ + public void setFileSystemPath(String fileSystemPath) + { + this.fileSystemPath = fileSystemPath; + } + + + + /******************************************************************************* + ** Fluent setter for fileSystemPath + *******************************************************************************/ + public JavalinRouteProviderMetaData withFileSystemPath(String fileSystemPath) + { + this.fileSystemPath = fileSystemPath; + return (this); + } + + + + /******************************************************************************* + ** Getter for processName + *******************************************************************************/ + public String getProcessName() + { + return (this.processName); + } + + + + /******************************************************************************* + ** Setter for processName + *******************************************************************************/ + public void setProcessName(String processName) + { + this.processName = processName; + } + + + + /******************************************************************************* + ** Fluent setter for processName + *******************************************************************************/ + public JavalinRouteProviderMetaData withProcessName(String processName) + { + this.processName = processName; + return (this); + } + + + + /******************************************************************************* + ** Getter for methods + *******************************************************************************/ + public List getMethods() + { + return (this.methods); + } + + + + /******************************************************************************* + ** Setter for methods + *******************************************************************************/ + public void setMethods(List methods) + { + this.methods = methods; + } + + + + /******************************************************************************* + ** Fluent setter for methods + *******************************************************************************/ + public JavalinRouteProviderMetaData withMethods(List methods) + { + this.methods = methods; + return (this); + } + + + + /******************************************************************************* + ** Getter for routeAuthenticator + *******************************************************************************/ + public QCodeReference getRouteAuthenticator() + { + return (this.routeAuthenticator); + } + + + + /******************************************************************************* + ** Setter for routeAuthenticator + *******************************************************************************/ + public void setRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + } + + + + /******************************************************************************* + ** Fluent setter for routeAuthenticator + *******************************************************************************/ + public JavalinRouteProviderMetaData withRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + return (this); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/misc/DownloadFileSupplementalAction.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/misc/DownloadFileSupplementalAction.java index 3ab31747..2fd39d5c 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/misc/DownloadFileSupplementalAction.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/misc/DownloadFileSupplementalAction.java @@ -193,6 +193,16 @@ public interface DownloadFileSupplementalAction ***************************************************************************/ class DownloadFileSupplementalActionOutput { - + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public DownloadFileSupplementalActionOutput() + { + //////////////////////////////////////////////////////////////// + // sorry, but here just to get test-coverage on this class... // + //////////////////////////////////////////////////////////////// + int i = 0; + } } } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java new file mode 100644 index 00000000..02dad90b --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouter.java @@ -0,0 +1,303 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.routeproviders; + + +import java.io.InputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.actions.tables.StorageAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import com.kingsrook.qqq.backend.javalin.QJavalinUtils; +import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface; +import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; +import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface; +import io.javalin.apibuilder.ApiBuilder; +import io.javalin.apibuilder.EndpointGroup; +import io.javalin.http.Context; +import io.javalin.http.HttpStatus; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ProcessBasedRouter implements QJavalinRouteProviderInterface +{ + private static final QLogger LOG = QLogger.getLogger(ProcessBasedRouter.class); + + private final String hostedPath; + private final String processName; + private final List methods; + + private QCodeReference routeAuthenticator; + + private QInstance qInstance; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessBasedRouter(String hostedPath, String processName) + { + this(hostedPath, processName, null); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public ProcessBasedRouter(JavalinRouteProviderMetaData routeProvider) + { + this(routeProvider.getHostedPath(), routeProvider.getProcessName(), routeProvider.getMethods()); + setRouteAuthenticator(routeProvider.getRouteAuthenticator()); + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessBasedRouter(String hostedPath, String processName, List methods) + { + this.hostedPath = hostedPath; + this.processName = processName; + + if(CollectionUtils.nullSafeHasContents(methods)) + { + this.methods = methods; + } + else + { + this.methods = List.of("GET"); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void setQInstance(QInstance qInstance) + { + this.qInstance = qInstance; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public EndpointGroup getJavalinEndpointGroup() + { + return (() -> + { + for(String method : methods) + { + switch(method.toLowerCase()) + { + case "get" -> ApiBuilder.get(hostedPath, this::handleRequest); + case "post" -> ApiBuilder.post(hostedPath, this::handleRequest); + case "put" -> ApiBuilder.put(hostedPath, this::handleRequest); + case "patch" -> ApiBuilder.patch(hostedPath, this::handleRequest); + case "delete" -> ApiBuilder.delete(hostedPath, this::handleRequest); + default -> throw (new IllegalArgumentException("Unrecognized method: " + method)); + } + } + }); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void handleRequest(Context context) + { + RunProcessInput input = new RunProcessInput(); + input.setProcessName(processName); + + QContext.init(qInstance, new QSystemUserSession()); + + boolean isAuthenticated = false; + if(routeAuthenticator == null) + { + isAuthenticated = true; + } + else + { + try + { + RouteAuthenticatorInterface routeAuthenticator = QCodeLoader.getAdHoc(RouteAuthenticatorInterface.class, this.routeAuthenticator); + isAuthenticated = routeAuthenticator.authenticateRequest(context); + } + catch(Exception e) + { + context.skipRemainingHandlers(); + QJavalinImplementation.handleException(context, e); + } + } + + if(!isAuthenticated) + { + LOG.info("Request is not authenticated, so returning before running process", logPair("processName", processName), logPair("path", context.path())); + return; + } + + try + { + LOG.info("Running process to serve route", logPair("processName", processName), logPair("path", context.path())); + + ///////////////////// + // run the process // + ///////////////////// + input.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP); + input.addValue("path", context.path()); + input.addValue("method", context.method()); + input.addValue("pathParams", new HashMap<>(context.pathParamMap())); + input.addValue("queryParams", new HashMap<>(context.queryParamMap())); + input.addValue("formParams", new HashMap<>(context.formParamMap())); + input.addValue("cookies", new HashMap<>(context.cookieMap())); + input.addValue("requestHeaders", new HashMap<>(context.headerMap())); + + RunProcessOutput runProcessOutput = new RunProcessAction().execute(input); + + ///////////////// + // headers map // + ///////////////// + Serializable headers = runProcessOutput.getValue("responseHeaders"); + if(headers instanceof Map headersMap) + { + for(Object key : headersMap.keySet()) + { + context.header(ValueUtils.getValueAsString(key), ValueUtils.getValueAsString(headersMap.get(key))); + } + } + + // todo - make the inputStream available to the process + // maybe via the callback object??? input.setCallback(new QProcessCallback() {}); + // context.resultInputStream(); + + ////////////// + // response // + ////////////// + Integer statusCode = runProcessOutput.getValueInteger("statusCode"); + String redirectURL = runProcessOutput.getValueString("redirectURL"); + String responseString = runProcessOutput.getValueString("responseString"); + byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes"); + StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput"); + + if(StringUtils.hasContent(redirectURL)) + { + context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode)); + return; + } + + if(statusCode != null) + { + context.status(statusCode); + } + + if(StringUtils.hasContent(responseString)) + { + context.result(responseString); + return; + } + + if(responseBytes != null && responseBytes.length > 0) + { + context.result(responseBytes); + return; + } + + if(responseStorageInput != null) + { + InputStream inputStream = new StorageAction().getInputStream(responseStorageInput); + context.result(inputStream); + return; + } + + LOG.debug("No response value was set in the process output state."); + } + catch(Exception e) + { + QJavalinUtils.handleException(null, context, e); + } + finally + { + QContext.clear(); + } + } + + + + /******************************************************************************* + ** Getter for routeAuthenticator + *******************************************************************************/ + public QCodeReference getRouteAuthenticator() + { + return (this.routeAuthenticator); + } + + + + /******************************************************************************* + ** Setter for routeAuthenticator + *******************************************************************************/ + public void setRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + } + + + + /******************************************************************************* + ** Fluent setter for routeAuthenticator + *******************************************************************************/ + public ProcessBasedRouter withRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + return (this); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java new file mode 100644 index 00000000..8d754469 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/ProcessBasedRouterPayload.java @@ -0,0 +1,413 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.routeproviders; + + +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; +import com.kingsrook.qqq.backend.core.model.actions.processes.QProcessPayload; + + +/******************************************************************************* + ** process payload shared the processes which are used as process-based-router + ** processes. e.g., the fields here are those written to and read by + ** ProcessBasedRouter. + *******************************************************************************/ +public class ProcessBasedRouterPayload extends QProcessPayload +{ + private String path; + private String method; + private Map pathParams; + private Map> queryParams; + private Map> formParams; + private Map cookies; + + private Integer statusCode; + private String redirectURL; + private Map responseHeaders; + private String responseString; + private byte[] responseBytes; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessBasedRouterPayload() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ProcessBasedRouterPayload(ProcessState processState) + { + this.populateFromProcessState(processState); + } + + + + /******************************************************************************* + ** Getter for path + *******************************************************************************/ + public String getPath() + { + return (this.path); + } + + + + /******************************************************************************* + ** Setter for path + *******************************************************************************/ + public void setPath(String path) + { + this.path = path; + } + + + + /******************************************************************************* + ** Fluent setter for path + *******************************************************************************/ + public ProcessBasedRouterPayload withPath(String path) + { + this.path = path; + return (this); + } + + + + /******************************************************************************* + ** Getter for method + *******************************************************************************/ + public String getMethod() + { + return (this.method); + } + + + + /******************************************************************************* + ** Setter for method + *******************************************************************************/ + public void setMethod(String method) + { + this.method = method; + } + + + + /******************************************************************************* + ** Fluent setter for method + *******************************************************************************/ + public ProcessBasedRouterPayload withMethod(String method) + { + this.method = method; + return (this); + } + + + + /******************************************************************************* + ** Getter for pathParams + *******************************************************************************/ + public Map getPathParams() + { + return (this.pathParams); + } + + + + /******************************************************************************* + ** Setter for pathParams + *******************************************************************************/ + public void setPathParams(Map pathParams) + { + this.pathParams = pathParams; + } + + + + /******************************************************************************* + ** Fluent setter for pathParams + *******************************************************************************/ + public ProcessBasedRouterPayload withPathParams(Map pathParams) + { + this.pathParams = pathParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for cookies + *******************************************************************************/ + public Map getCookies() + { + return (this.cookies); + } + + + + /******************************************************************************* + ** Setter for cookies + *******************************************************************************/ + public void setCookies(Map cookies) + { + this.cookies = cookies; + } + + + + /******************************************************************************* + ** Fluent setter for cookies + *******************************************************************************/ + public ProcessBasedRouterPayload withCookies(Map cookies) + { + this.cookies = cookies; + return (this); + } + + + + /******************************************************************************* + ** Getter for statusCode + *******************************************************************************/ + public Integer getStatusCode() + { + return (this.statusCode); + } + + + + /******************************************************************************* + ** Setter for statusCode + *******************************************************************************/ + public void setStatusCode(Integer statusCode) + { + this.statusCode = statusCode; + } + + + + /******************************************************************************* + ** Fluent setter for statusCode + *******************************************************************************/ + public ProcessBasedRouterPayload withStatusCode(Integer statusCode) + { + this.statusCode = statusCode; + return (this); + } + + + + /******************************************************************************* + ** Getter for responseHeaders + *******************************************************************************/ + public Map getResponseHeaders() + { + return (this.responseHeaders); + } + + + + /******************************************************************************* + ** Setter for responseHeaders + *******************************************************************************/ + public void setResponseHeaders(Map responseHeaders) + { + this.responseHeaders = responseHeaders; + } + + + + /******************************************************************************* + ** Fluent setter for responseHeaders + *******************************************************************************/ + public ProcessBasedRouterPayload withResponseHeaders(Map responseHeaders) + { + this.responseHeaders = responseHeaders; + return (this); + } + + + + /******************************************************************************* + ** Getter for responseString + *******************************************************************************/ + public String getResponseString() + { + return (this.responseString); + } + + + + /******************************************************************************* + ** Setter for responseString + *******************************************************************************/ + public void setResponseString(String responseString) + { + this.responseString = responseString; + } + + + + /******************************************************************************* + ** Fluent setter for responseString + *******************************************************************************/ + public ProcessBasedRouterPayload withResponseString(String responseString) + { + this.responseString = responseString; + return (this); + } + + + + /******************************************************************************* + ** Getter for responseBytes + *******************************************************************************/ + public byte[] getResponseBytes() + { + return (this.responseBytes); + } + + + + /******************************************************************************* + ** Setter for responseBytes + *******************************************************************************/ + public void setResponseBytes(byte[] responseBytes) + { + this.responseBytes = responseBytes; + } + + + + /******************************************************************************* + ** Fluent setter for responseBytes + *******************************************************************************/ + public ProcessBasedRouterPayload withResponseBytes(byte[] responseBytes) + { + this.responseBytes = responseBytes; + return (this); + } + + + + /******************************************************************************* + ** Getter for redirectURL + *******************************************************************************/ + public String getRedirectURL() + { + return (this.redirectURL); + } + + + + /******************************************************************************* + ** Setter for redirectURL + *******************************************************************************/ + public void setRedirectURL(String redirectURL) + { + this.redirectURL = redirectURL; + } + + + + /******************************************************************************* + ** Fluent setter for redirectURL + *******************************************************************************/ + public ProcessBasedRouterPayload withRedirectURL(String redirectURL) + { + this.redirectURL = redirectURL; + return (this); + } + + + + /******************************************************************************* + ** Getter for queryParams + *******************************************************************************/ + public Map> getQueryParams() + { + return (this.queryParams); + } + + + + /******************************************************************************* + ** Setter for queryParams + *******************************************************************************/ + public void setQueryParams(Map> queryParams) + { + this.queryParams = queryParams; + } + + + + /******************************************************************************* + ** Fluent setter for queryParams + *******************************************************************************/ + public ProcessBasedRouterPayload withQueryParams(Map> queryParams) + { + this.queryParams = queryParams; + return (this); + } + + + + /******************************************************************************* + ** Getter for formParams + *******************************************************************************/ + public Map> getFormParams() + { + return (this.formParams); + } + + + + /******************************************************************************* + ** Setter for formParams + *******************************************************************************/ + public void setFormParams(Map> formParams) + { + this.formParams = formParams; + } + + + + /******************************************************************************* + ** Fluent setter for formParams + *******************************************************************************/ + public ProcessBasedRouterPayload withFormParams(Map> formParams) + { + this.formParams = formParams; + return (this); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java new file mode 100644 index 00000000..c6504be1 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/SimpleFileSystemDirectoryRouter.java @@ -0,0 +1,224 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.routeproviders; + + +import java.net.URL; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface; +import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; +import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface; +import io.javalin.Javalin; +import io.javalin.config.JavalinConfig; +import io.javalin.http.Context; +import io.javalin.http.staticfiles.Location; +import io.javalin.http.staticfiles.StaticFileConfig; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** javalin route provider that hosts a path in the http server via a path on + ** the file system + *******************************************************************************/ +public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInterface +{ + private static final QLogger LOG = QLogger.getLogger(SimpleFileSystemDirectoryRouter.class); + + private final String hostedPath; + private final String fileSystemPath; + + private QCodeReference routeAuthenticator; + + private QInstance qInstance; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public SimpleFileSystemDirectoryRouter(String hostedPath, String fileSystemPath) + { + this.hostedPath = hostedPath; + this.fileSystemPath = fileSystemPath; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + public SimpleFileSystemDirectoryRouter(JavalinRouteProviderMetaData routeProvider) + { + this(routeProvider.getHostedPath(), routeProvider.getFileSystemPath()); + setRouteAuthenticator(routeProvider.getRouteAuthenticator()); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void setQInstance(QInstance qInstance) + { + this.qInstance = qInstance; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void handleJavalinStaticFileConfig(StaticFileConfig staticFileConfig) + { + URL resource = getClass().getClassLoader().getResource(fileSystemPath); + if(resource == null) + { + String message = "Could not find file system path: " + fileSystemPath; + if(fileSystemPath.startsWith("/") && getClass().getClassLoader().getResource(fileSystemPath.replaceFirst("^/+", "")) != null) + { + message += ". For non-absolute paths, do not prefix with a leading slash."; + } + throw new RuntimeException(message); + } + + if(!hostedPath.startsWith("/")) + { + LOG.warn("hostedPath [" + hostedPath + "] should probably start with a leading slash..."); + } + + staticFileConfig.directory = resource.getFile(); + staticFileConfig.hostedPath = hostedPath; + staticFileConfig.location = Location.EXTERNAL; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void before(Context context) throws QException + { + LOG.debug("In before handler for simpleFileSystemRouter", logPair("hostedPath", hostedPath)); + QContext.init(qInstance, new QSystemUserSession()); + + if(routeAuthenticator != null) + { + try + { + RouteAuthenticatorInterface routeAuthenticator = QCodeLoader.getAdHoc(RouteAuthenticatorInterface.class, this.routeAuthenticator); + boolean isAuthenticated = routeAuthenticator.authenticateRequest(context); + if(!isAuthenticated) + { + LOG.info("Static file request is not authenticated, so telling javalin to skip remaining handlers", logPair("path", context.path())); + context.skipRemainingHandlers(); + } + } + catch(Exception e) + { + context.skipRemainingHandlers(); + QJavalinImplementation.handleException(context, e); + } + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private void after(Context context) + { + LOG.debug("In after handler for simpleFileSystemRouter", logPair("hostedPath", hostedPath)); + QContext.clear(); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void acceptJavalinConfig(JavalinConfig config) + { + config.staticFiles.add(this::handleJavalinStaticFileConfig); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void acceptJavalinService(Javalin service) + { + String javalinPath = hostedPath; + if(!javalinPath.endsWith("/")) + { + javalinPath += "/"; + } + javalinPath += ""; + + service.before(javalinPath, this::before); + service.before(javalinPath, this::after); + } + + + + /******************************************************************************* + ** Getter for routeAuthenticator + *******************************************************************************/ + public QCodeReference getRouteAuthenticator() + { + return (this.routeAuthenticator); + } + + + + /******************************************************************************* + ** Setter for routeAuthenticator + *******************************************************************************/ + public void setRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + } + + + + /******************************************************************************* + ** Fluent setter for routeAuthenticator + *******************************************************************************/ + public SimpleFileSystemDirectoryRouter withRouteAuthenticator(QCodeReference routeAuthenticator) + { + this.routeAuthenticator = routeAuthenticator; + return (this); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/RouteAuthenticatorInterface.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/RouteAuthenticatorInterface.java new file mode 100644 index 00000000..c08b09ef --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/RouteAuthenticatorInterface.java @@ -0,0 +1,43 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.routeproviders.authentication; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import io.javalin.http.Context; + + +/******************************************************************************* + ** interface used by QJavalinRouteProviderInterface subclasses, to interact with + ** QQQ Authentication modules, to provide authentication to custom javalin routes. + *******************************************************************************/ +public interface RouteAuthenticatorInterface +{ + + /*************************************************************************** + ** where authentication for a route occurs, before the route is served. + ** + ** @return true if request is authenticated; else false + ***************************************************************************/ + boolean authenticateRequest(Context context) throws QException; + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java new file mode 100644 index 00000000..82f83024 --- /dev/null +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java @@ -0,0 +1,94 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.middleware.javalin.routeproviders.authentication; + + +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; +import io.javalin.http.Context; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** simple implementation of a route authenticator. Assumes that unauthenticated + ** requests should redirect to a login page. Note though, maybe that should be + ** more intelligent, like, only redirect requests for a .html file, but not + ** requests for include files like images or .js/.css? + *******************************************************************************/ +public class SimpleRouteAuthenticator implements RouteAuthenticatorInterface +{ + private static final QLogger LOG = QLogger.getLogger(SimpleRouteAuthenticator.class); + + + /*************************************************************************** + ** + ***************************************************************************/ + public boolean authenticateRequest(Context context) throws QException + { + try + { + QSession qSession = QJavalinImplementation.setupSession(context, null); + LOG.debug("Session has been activated", logPair("uuid", qSession.getUuid())); + + if(context.queryParamMap().containsKey("code") && context.queryParamMap().containsKey("state")) + { + ////////////////////////////////////////////////////////////////////////// + // if this request was a callback from oauth, with code & state params, // + // then redirect one last time removing those from the query string // + ////////////////////////////////////////////////////////////////////////// + String redirectURL = context.fullUrl().replace("code=" + context.queryParam("code"), "") + .replace("state=" + context.queryParam("state"), "") + .replaceFirst("&+$", "") + .replaceFirst("\\?&", "?") + .replaceFirst("\\?$", ""); + context.redirect(redirectURL); + LOG.debug("Redirecting request to remove code and state parameters"); + return (false); + } + + return (true); + } + catch(QAuthenticationException e) + { + QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(QContext.getQInstance().getAuthentication()); + + String redirectURL = authenticationModule.getLoginRedirectUrl(context.fullUrl()); + + context.redirect(redirectURL); + LOG.debug("Redirecting request, due to required session missing"); + return (false); + } + catch(QModuleDispatchException e) + { + throw (new QException("Error authenticating request", e)); + } + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java index cfb4c22e..5c625b1e 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/MetaDataSpecV1.java @@ -242,23 +242,29 @@ public class MetaDataSpecV1 extends AbstractEndpointSpec + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // double-wrap the context here, so the instance will exist when the system-user-session is created // + // to avoid warnings out of system-user-session about there not being an instance in context. // + ////////////////////////////////////////////////////////////////////////////////////////////////////// + QContext.withTemporaryContext(new CapturedContext(exampleInstance, null), () -> { - try + QContext.withTemporaryContext(new CapturedContext(exampleInstance, new QSystemUserSession()), () -> { - MetaDataAction metaDataAction = new MetaDataAction(); - MetaDataOutput output = metaDataAction.execute(new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput()); - examples.put("Example", new Example() - .withValue(new MetaDataResponseV1() - .withMetaDataOutput(output) - ) - ); - } - catch(Exception e) - { - examples.put("Example", new Example().withValue("Error building example: " + e.getMessage()) - ); - } + try + { + MetaDataAction metaDataAction = new MetaDataAction(); + MetaDataOutput output = metaDataAction.execute(new com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput()); + examples.put("Example", new Example() + .withValue(new MetaDataResponseV1() + .withMetaDataOutput(output) + ) + ); + } + catch(Exception e) + { + examples.put("Example", new Example().withValue("Error building example: " + e.getMessage())); + } + }); }); return new BasicResponse(""" diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index 6773f4fe..59ae8177 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -28,6 +28,7 @@ import java.sql.Connection; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; @@ -95,6 +96,9 @@ import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackend import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; +import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouterPayload; +import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.SimpleRouteAuthenticator; import org.apache.commons.io.IOUtils; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -123,18 +127,17 @@ public class TestUtils public static final String SCREEN_0 = "screen0"; public static final String SCREEN_1 = "screen1"; + public static final String STATIC_SITE_PATH = "static-site"; + /******************************************************************************* ** Prime a test database (e.g., h2, in-memory) ** *******************************************************************************/ - @SuppressWarnings("unchecked") public static void primeTestDatabase() throws Exception { - ConnectionManager connectionManager = new ConnectionManager(); - - try(Connection connection = connectionManager.getConnection(TestUtils.defineDefaultH2Backend())) + try(Connection connection = ConnectionManager.getConnection(TestUtils.defineDefaultH2Backend())) { InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql"); assertNotNull(primeTestDatabaseSqlStream); @@ -156,8 +159,7 @@ public class TestUtils *******************************************************************************/ public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception { - ConnectionManager connectionManager = new ConnectionManager(); - try(Connection connection = connectionManager.getConnection(defineDefaultH2Backend())) + try(Connection connection = ConnectionManager.getConnection(defineDefaultH2Backend())) { QueryManager.executeStatement(connection, sql, resultSetProcessor); } @@ -184,10 +186,33 @@ public class TestUtils qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessPutsNullKeyInMap()); qInstance.addProcess(defineProcessSimpleThrow()); + qInstance.addProcess(defineRouterProcess()); qInstance.addReport(definePersonsReport()); qInstance.addPossibleValueSource(definePossibleValueSourcePerson()); defineWidgets(qInstance); + List routeProviders = new ArrayList<>(); + routeProviders.add(new JavalinRouteProviderMetaData() + .withHostedPath("/statically-served") + .withFileSystemPath(STATIC_SITE_PATH)); + + routeProviders.add(new JavalinRouteProviderMetaData() + .withHostedPath("/protected-statically-served") + .withFileSystemPath(STATIC_SITE_PATH) + .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class))); + + routeProviders.add(new JavalinRouteProviderMetaData() + .withHostedPath("/served-by-process/") + .withMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE")) + .withProcessName("routerProcess")); + + routeProviders.add(new JavalinRouteProviderMetaData() + .withHostedPath("/protected-served-by-process/") + .withProcessName("routerProcess") + .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class))); + + qInstance.withSupplementalMetaData(new QJavalinMetaData().withRouteProviders(routeProviders)); + qInstance.addBackend(defineMemoryBackend()); try { @@ -206,6 +231,55 @@ public class TestUtils + /*************************************************************************** + * + ***************************************************************************/ + private static QProcessMetaData defineRouterProcess() + { + return (new QProcessMetaData() + .withName("routerProcess") + .withStep(new QBackendStepMetaData() + .withName("step") + .withCode(new QCodeReferenceLambda((runBackendStepInput, runBackendStepOutput) -> + { + ProcessBasedRouterPayload processPayload = runBackendStepInput.getProcessPayload(ProcessBasedRouterPayload.class); + String path = processPayload.getPath(); + + if(processPayload.getQueryParams().containsKey("requestedRedirect")) + { + processPayload.setRedirectURL(processPayload.getQueryParams().get("requestedRedirect").get(0)); + } + else + { + String response = "So you've done a " + processPayload.getMethod() + " for: " + path; + if(processPayload.getQueryParams().containsKey("respondInBytes")) + { + processPayload.setResponseBytes(response.getBytes(StandardCharsets.UTF_8)); + } + else if(processPayload.getQueryParams().containsKey("noResponse")) + { + /////////////////////////////////////// + // don't call any setResponse method // + /////////////////////////////////////// + } + else if(processPayload.getQueryParams().containsKey("doThrow")) + { + throw (new QException("Test Exception")); + } + else + { + processPayload.setResponseString(response); + } + processPayload.setResponseHeaders(Map.of("X-Test", "Yes, Test")); + processPayload.setStatusCode(200); + } + runBackendStepOutput.setProcessPayload(processPayload); + })) + )); + } + + + /*************************************************************************** ** ***************************************************************************/ @@ -567,7 +641,6 @@ public class TestUtils - /******************************************************************************* ** Define an interactive version of the 'greet people' process *******************************************************************************/ @@ -587,6 +660,7 @@ public class TestUtils } + /******************************************************************************* ** Define a process with just one step that sleeps and then throws *******************************************************************************/ @@ -754,7 +828,7 @@ public class TestUtils { return (new RenderWidgetOutput(new RawHTML("title", QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE_OFFSET_MINUTES) - + "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE) + + "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE) ))); } } diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java index a1cdf6b2..a6bf4085 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServerTest.java @@ -22,18 +22,22 @@ package com.kingsrook.qqq.middleware.javalin; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.List; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.javalin.TestUtils; import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; +import io.javalin.http.HttpStatus; import kong.unirest.HttpResponse; import kong.unirest.Unirest; import org.json.JSONObject; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -52,7 +56,7 @@ class QApplicationJavalinServerTest ** *******************************************************************************/ @AfterEach - void afterEach() + void afterEach() throws IOException { javalinServer.stop(); TestApplication.callCount = 0; @@ -123,6 +127,7 @@ class QApplicationJavalinServerTest } + /******************************************************************************* ** *******************************************************************************/ @@ -173,6 +178,124 @@ class QApplicationJavalinServerTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testStaticRouter() throws Exception + { + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withServeFrontendMaterialDashboard(false) + .withPort(PORT); + javalinServer.start(); + + Unirest.config().setDefaultResponseEncoding("UTF-8"); + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/statically-served/foo.html").asString(); + assertEquals("Foo? Bar!", response.getBody()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthenticatedStaticRouter() throws Exception + { + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withServeFrontendMaterialDashboard(false) + .withPort(PORT); + javalinServer.start(); + + Unirest.config().setDefaultResponseEncoding("UTF-8") + .reset() + .followRedirects(false); + + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html") + .header("Authorization", "Bearer Deny") + .asString(); + + assertEquals(HttpStatus.FOUND.getCode(), response.getStatus()); + assertThat(response.getHeaders().getFirst("Location")).contains("createMockSession"); + + response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html") + .asString(); + assertEquals(HttpStatus.OK.getCode(), response.getStatus()); + assertEquals("Foo? Bar!", response.getBody()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcessRouter() throws Exception + { + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withServeFrontendMaterialDashboard(false) + .withPort(PORT); + javalinServer.start(); + + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/served-by-process/foo.html").asString(); + assertEquals(200, response.getStatus()); + assertEquals("So you've done a GET for: /served-by-process/foo.html", response.getBody()); + + response = Unirest.post("http://localhost:" + PORT + "/served-by-process/foo.html").asString(); + assertEquals(200, response.getStatus()); + assertEquals("So you've done a POST for: /served-by-process/foo.html", response.getBody()); + assertEquals("Yes, Test", response.getHeaders().getFirst("X-Test")); + + response = Unirest.put("http://localhost:" + PORT + "/served-by-process/foo.html?requestedRedirect=google.com").asString(); + assertEquals(302, response.getStatus()); + assertEquals("google.com", response.getHeaders().getFirst("Location")); + + HttpResponse responseBytes = Unirest.delete("http://localhost:" + PORT + "/served-by-process/foo.html?respondInBytes=true").asBytes(); + assertEquals(200, responseBytes.getStatus()); + assertArrayEquals("So you've done a DELETE for: /served-by-process/foo.html".getBytes(StandardCharsets.UTF_8), responseBytes.getBody()); + + response = Unirest.get("http://localhost:" + PORT + "/served-by-process/foo.html?noResponse=true").asString(); + assertEquals(200, response.getStatus()); + assertEquals("", response.getBody()); + + response = Unirest.get("http://localhost:" + PORT + "/served-by-process/foo.html?doThrow=true").asString(); + assertEquals(500, response.getStatus()); + assertThat(response.getBody()).contains("Test Exception"); + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthenticatedProcessRouter() throws Exception + { + javalinServer = new QApplicationJavalinServer(getQqqApplication()) + .withServeFrontendMaterialDashboard(false) + .withPort(PORT); + javalinServer.start(); + + Unirest.config().setDefaultResponseEncoding("UTF-8") + .reset() + .followRedirects(false); + + HttpResponse response = Unirest.get("http://localhost:" + PORT + "/protected-served-by-process/foo.html") + .header("Authorization", "Bearer Deny") + .asString(); + + assertEquals(HttpStatus.FOUND.getCode(), response.getStatus()); + assertThat(response.getHeaders().getFirst("Location")).contains("createMockSession"); + + response = Unirest.get("http://localhost:" + PORT + "/protected-served-by-process/foo.html") + .asString(); + assertEquals(200, response.getStatus()); + assertEquals("So you've done a GET for: /protected-served-by-process/foo.html", response.getBody()); + } + + + /*************************************************************************** ** ***************************************************************************/ diff --git a/qqq-middleware-javalin/src/test/resources/static-site/foo.html b/qqq-middleware-javalin/src/test/resources/static-site/foo.html new file mode 100644 index 00000000..22b50adf --- /dev/null +++ b/qqq-middleware-javalin/src/test/resources/static-site/foo.html @@ -0,0 +1 @@ +Foo? Bar! \ No newline at end of file diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/ConfigFileBasedSampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/ConfigFileBasedSampleJavalinServer.java new file mode 100644 index 00000000..07973c3a --- /dev/null +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/ConfigFileBasedSampleJavalinServer.java @@ -0,0 +1,82 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.sampleapp; + + +import java.util.Arrays; +import com.kingsrook.qqq.backend.core.instances.ConfigFilesBasedQQQApplication; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer; +import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ConfigFileBasedSampleJavalinServer +{ + private static final QLogger LOG = QLogger.getLogger(ConfigFileBasedSampleJavalinServer.class); + private final String path; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void main(String[] args) + { + String path = "src/main/resources/metadata"; + if(args.length > 0) + { + path = args[0]; + System.out.println("Using path from args [" + path + "]"); + } + + new ConfigFileBasedSampleJavalinServer(path).start(); + } + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public ConfigFileBasedSampleJavalinServer(String path) + { + this.path = path; + } + + + /******************************************************************************* + ** + *******************************************************************************/ + public void start() + { + try + { + new QApplicationJavalinServer(new ConfigFilesBasedQQQApplication(path)).start(); + } + catch(Exception e) + { + LOG.error("Failed to start javalin server. See stack trace for details.", e); + } + } + +} diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java index cd828001..fd1b468b 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java @@ -50,7 +50,25 @@ public class SampleCli { try { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineInstance(); + return (run(qInstance, args)); + } + catch(Exception e) + { + e.printStackTrace(); + return (-1); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + int run(QInstance qInstance, String[] args) + { + try + { QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); return (qPicoCliImplementation.runCli("my-sample-cli", args)); diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java index 4ab4f375..83753e55 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleJavalinServer.java @@ -25,6 +25,7 @@ package com.kingsrook.sampleapp; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer; import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider; +import static com.kingsrook.sampleapp.metadata.SampleMetaDataProvider.primeTestDatabase; /******************************************************************************* @@ -53,7 +54,11 @@ public class SampleJavalinServer { try { - new QApplicationJavalinServer(new SampleMetaDataProvider()).start(); + primeTestDatabase("prime-test-database.sql"); + + QApplicationJavalinServer javalinServer = new QApplicationJavalinServer(new SampleMetaDataProvider()); + + javalinServer.start(); } catch(Exception e) { diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/DynamicSiteProcessMetaDataProducer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/DynamicSiteProcessMetaDataProducer.java new file mode 100644 index 00000000..92d65133 --- /dev/null +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/DynamicSiteProcessMetaDataProducer.java @@ -0,0 +1,56 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.sampleapp.metadata; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.sampleapp.processes.dynamicsite.DynamicSiteProcessStep; + + +/******************************************************************************* + ** Meta Data Producer for DynamicSiteProcess + *******************************************************************************/ +public class DynamicSiteProcessMetaDataProducer extends MetaDataProducer +{ + public static final String NAME = "DynamicSiteProcess"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QProcessMetaData produce(QInstance qInstance) throws QException + { + return (new QProcessMetaData() + .withName(NAME) + .withStep(new QBackendStepMetaData() + .withName("DynamicSiteProcessStep") + .withCode(new QCodeReference(DynamicSiteProcessStep.class)))); + } + +} diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java new file mode 100644 index 00000000..49a30db3 --- /dev/null +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java @@ -0,0 +1,67 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.sampleapp.metadata; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.OAuth2AuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.RedirectStateMetaDataProducer; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession; + + +/******************************************************************************* + ** Provides all OAuth2 authentication related metadata to the QQQ engine + * + *******************************************************************************/ +public class OAuth2MetaDataProvider implements MetaDataProducerInterface +{ + public static final String NAME = "OAuth2"; + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QAuthenticationMetaData produce(QInstance qInstance) throws QException + { + QMetaDataVariableInterpreter qMetaDataVariableInterpreter = new QMetaDataVariableInterpreter(); + + String oauth2BaseUrl = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_BASE_URL}"); + String oauth2ClientId = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_CLIENT_ID}"); + String oauth2ClientSecret = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_CLIENT_SECRET}"); + String oauth2Scopes = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_SCOPES}"); + + return (new OAuth2AuthenticationMetaData() + .withBaseUrl(oauth2BaseUrl) + .withClientId(oauth2ClientId) + .withClientSecret(oauth2ClientSecret) + .withScopes(oauth2Scopes) + .withUserSessionTableName(UserSession.TABLE_NAME) + .withRedirectStateTableName(RedirectStateMetaDataProducer.TABLE_NAME) + .withName(NAME)); + } +} diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleJavalinMetaDataProducer.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleJavalinMetaDataProducer.java new file mode 100644 index 00000000..602daec1 --- /dev/null +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleJavalinMetaDataProducer.java @@ -0,0 +1,64 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.sampleapp.metadata; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.javalin.QJavalinMetaData; +import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData; +import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.SimpleRouteAuthenticator; + + +/******************************************************************************* + ** Meta Data Producer for SampleJavalin + *******************************************************************************/ +public class SampleJavalinMetaDataProducer extends MetaDataProducer +{ + + /******************************************************************************* + ** todo wip - test sub-directories of each other + ** todo wip - allow mat-dash to be served at a different path + ** todo wip - get mat-dash committed + *******************************************************************************/ + @Override + public QJavalinMetaData produce(QInstance qInstance) throws QException + { + return (new QJavalinMetaData() + .withRouteProvider(new JavalinRouteProviderMetaData() + .withHostedPath("/public") + .withFileSystemPath("site/public")) + + .withRouteProvider(new JavalinRouteProviderMetaData() + .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class)) + .withHostedPath("/private") + .withFileSystemPath("site/private")) + + .withRouteProvider(new JavalinRouteProviderMetaData() + .withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class)) + .withHostedPath("/dynamic-site/") + .withProcessName(DynamicSiteProcessMetaDataProducer.NAME))); + } + +} diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java index bc40d1a1..03df856c 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; @@ -71,6 +72,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.RedirectStateMetaDataProducer; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.UserSessionMetaDataProducer; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; @@ -83,6 +87,7 @@ import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.Filesyst import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; import com.kingsrook.sampleapp.dashboard.widgets.PersonsByCreateDateBarChart; import com.kingsrook.sampleapp.processes.clonepeople.ClonePeopleTransformStep; import org.apache.commons.io.IOUtils; @@ -97,6 +102,7 @@ public class SampleMetaDataProvider extends AbstractQQQApplication public static final String RDBMS_BACKEND_NAME = "rdbms"; public static final String FILESYSTEM_BACKEND_NAME = "filesystem"; + public static final String MEMORY_BACKEND_NAME = "memory"; public static final String APP_NAME_GREETINGS = "greetingsApp"; public static final String APP_NAME_PEOPLE = "peopleApp"; @@ -140,8 +146,8 @@ public class SampleMetaDataProvider extends AbstractQQQApplication { QInstance qInstance = new QInstance(); - qInstance.setAuthentication(defineAuthentication()); qInstance.addBackend(defineRdbmsBackend()); + qInstance.addBackend(defineMemoryBackend()); qInstance.addBackend(defineFilesystemBackend()); qInstance.addTable(defineTableCarrier()); qInstance.addTable(defineTablePerson()); @@ -157,6 +163,9 @@ public class SampleMetaDataProvider extends AbstractQQQApplication qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessSimpleThrow()); + qInstance.addTable(setTableBackendNamesForRdbms(new UserSessionMetaDataProducer(RDBMS_BACKEND_NAME).produce(qInstance))); + qInstance.addTable(setTableBackendNamesForRdbms(new RedirectStateMetaDataProducer(RDBMS_BACKEND_NAME).produce(qInstance))); + MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, SampleMetaDataProvider.class.getPackageName()); defineWidgets(qInstance); @@ -168,6 +177,45 @@ public class SampleMetaDataProvider extends AbstractQQQApplication + /******************************************************************************* + ** if rdbms backend uses snake_case table & column names, instead of camelCase + ** style used for qqq meta-data tableNames and fieldNames, then set those via + ** this method. + *******************************************************************************/ + private static QTableMetaData setTableBackendNamesForRdbms(QTableMetaData table) + { + table.setBackendDetails(new RDBMSTableBackendDetails() + .withTableName(QInstanceEnricher.inferBackendName(table.getName()))); + QInstanceEnricher.setInferredFieldBackendNames(table); + return (table); + } + + + + /*************************************************************************** + ** for tests, define the same instance as above, but use mock authentication. + ***************************************************************************/ + public static QInstance defineTestInstance() throws QException + { + QInstance qInstance = defineInstance(); + qInstance.setAuthentication(defineAuthentication()); + return qInstance; + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private static QBackendMetaData defineMemoryBackend() + { + return new QBackendMetaData() + .withName(MEMORY_BACKEND_NAME) + .withBackendType(MemoryBackendModule.class); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/processes/dynamicsite/DynamicSiteProcessStep.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/processes/dynamicsite/DynamicSiteProcessStep.java new file mode 100644 index 00000000..bef228e1 --- /dev/null +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/processes/dynamicsite/DynamicSiteProcessStep.java @@ -0,0 +1,50 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.sampleapp.processes.dynamicsite; + + +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouterPayload; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class DynamicSiteProcessStep implements BackendStep +{ + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + ProcessBasedRouterPayload processPayload = runBackendStepInput.getProcessPayload(ProcessBasedRouterPayload.class); + + String path = processPayload.getPath(); + processPayload.setResponseString("You requested: " + path + "(at path-param: " + processPayload.getPathParams().get("pagePath") + ")"); + runBackendStepOutput.setProcessPayload(processPayload); + } + +} diff --git a/qqq-sample-project/src/main/resources/metadata/filesystemBackend.yaml b/qqq-sample-project/src/main/resources/metadata/filesystemBackend.yaml new file mode 100644 index 00000000..b190aab9 --- /dev/null +++ b/qqq-sample-project/src/main/resources/metadata/filesystemBackend.yaml @@ -0,0 +1,5 @@ +--- +class: FilesystemBackendMetaData +version: 1.0 +name: filesystem +basePath: /tmp/sample-filesystem \ No newline at end of file diff --git a/qqq-sample-project/src/main/resources/metadata/javalin.yaml b/qqq-sample-project/src/main/resources/metadata/javalin.yaml new file mode 100644 index 00000000..357e92bc --- /dev/null +++ b/qqq-sample-project/src/main/resources/metadata/javalin.yaml @@ -0,0 +1,4 @@ +--- +class: QJavalinMetaData +version: 1.0 +loggerDisabled: true \ No newline at end of file diff --git a/qqq-sample-project/src/main/resources/metadata/mockAuthentication.yaml b/qqq-sample-project/src/main/resources/metadata/mockAuthentication.yaml new file mode 100644 index 00000000..3188acdb --- /dev/null +++ b/qqq-sample-project/src/main/resources/metadata/mockAuthentication.yaml @@ -0,0 +1,5 @@ +--- +class: QAuthenticationMetaData +version: 1.0 +name: mock +type: MOCK \ No newline at end of file diff --git a/qqq-sample-project/src/main/resources/metadata/peopleApp.yaml b/qqq-sample-project/src/main/resources/metadata/peopleApp.yaml new file mode 100644 index 00000000..d4af4deb --- /dev/null +++ b/qqq-sample-project/src/main/resources/metadata/peopleApp.yaml @@ -0,0 +1,10 @@ +--- +class: QAppMetaData +version: 1.0 +name: people +icon: + name: person +sections: +- name: People + tables: + - person \ No newline at end of file diff --git a/qqq-sample-project/src/main/resources/metadata/personTable.yaml b/qqq-sample-project/src/main/resources/metadata/personTable.yaml new file mode 100644 index 00000000..1d8b0953 --- /dev/null +++ b/qqq-sample-project/src/main/resources/metadata/personTable.yaml @@ -0,0 +1,41 @@ +--- +class: QTableMetaData +version: 1.0 +name: person +backendName: rdbms +primaryKeyField: id +recordLabelFormat: "%s %s" +recordLabelFields: + - firstName + - lastName +fields: + id: + type: INTEGER + isEditable: false + firstName: + type: STRING + lastName: + type: STRING +## Field(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) +## Field(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false)) +## Field(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false)) +## Field(new QFieldMetaData("firstName", QFieldType.STRING).withBackendName("first_name").withIsRequired(true)) +## Field(new QFieldMetaData("lastName", QFieldType.STRING).withBackendName("last_name").withIsRequired(true)) +## Field(new QFieldMetaData("birthDate", QFieldType.DATE).withBackendName("birth_date")) +## Field(new QFieldMetaData("email", QFieldType.STRING)) +## Field(new QFieldMetaData("isEmployed", QFieldType.BOOLEAN).withBackendName("is_employed")) +## Field(new QFieldMetaData("annualSalary", QFieldType.DECIMAL).withBackendName("annual_salary").withDisplayFormat(DisplayFormat.CURRENCY)) +## Field(new QFieldMetaData("daysWorked", QFieldType.INTEGER).withBackendName("days_worked").withDisplayFormat(DisplayFormat.COMMAS)) +sections: +- name: identity + label: Identity + icon: + name: badge + tier: T1 + fieldNames: + - id + - firstName + - lastName +## Section(new QFieldSection("basicInfo", "Basic Info", new QIcon("dataset"), Tier.T2, List.of("email", "birthDate"))) +## Section(new QFieldSection("employmentInfo", "Employment Info", new QIcon("work"), Tier.T2, List.of("isEmployed", "annualSalary", "daysWorked"))) +## Section(new QFieldSection("dates", "Dates", new QIcon("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); diff --git a/qqq-sample-project/src/main/resources/metadata/rdbmsBackend.yaml b/qqq-sample-project/src/main/resources/metadata/rdbmsBackend.yaml new file mode 100644 index 00000000..58864338 --- /dev/null +++ b/qqq-sample-project/src/main/resources/metadata/rdbmsBackend.yaml @@ -0,0 +1,10 @@ +--- +class: RDBMSBackendMetaData +version: 1.0 +name: rdbms +vendor: ${env.RDBMS_VENDOR} +hostName: ${env.RDBMS_HOSTNAME} +port: ${env.RDBMS_PORT} +databaseName: ${env.RDBMS_DATABASE_NAME} +username: ${env.RDBMS_USERNAME} +password: ${env.RDBMS_PASSWORD} diff --git a/qqq-sample-project/src/main/resources/prime-test-database.sql b/qqq-sample-project/src/main/resources/prime-test-database.sql index 1f5e6bc9..5c0badc4 100644 --- a/qqq-sample-project/src/main/resources/prime-test-database.sql +++ b/qqq-sample-project/src/main/resources/prime-test-database.sql @@ -19,6 +19,31 @@ -- along with this program. If not, see . -- +DROP TABLE IF EXISTS user_session; +CREATE TABLE user_session +( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + create_date TIMESTAMP DEFAULT now(), + modify_date TIMESTAMP DEFAULT now(), + uuid VARCHAR(40) NOT NULL, + access_token MEDIUMTEXT, + user_id VARCHAR(100) +); +ALTER TABLE user_session ADD UNIQUE u_uuid (uuid); +ALTER TABLE user_session ADD INDEX i_user_id (user_id); + + +DROP TABLE IF EXISTS redirect_state; +CREATE TABLE redirect_state +( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + create_date TIMESTAMP DEFAULT now(), + state VARCHAR(45) NOT NULL, + redirect_uri TEXT +); +ALTER TABLE redirect_state ADD UNIQUE u_state (state); + + DROP TABLE IF EXISTS person; CREATE TABLE person ( diff --git a/qqq-sample-project/src/main/resources/site/private/index.html b/qqq-sample-project/src/main/resources/site/private/index.html new file mode 100644 index 00000000..104100b8 --- /dev/null +++ b/qqq-sample-project/src/main/resources/site/private/index.html @@ -0,0 +1,30 @@ + + + + +

Welcome to the private site

+ + + + diff --git a/qqq-sample-project/src/main/resources/site/public/index.html b/qqq-sample-project/src/main/resources/site/public/index.html new file mode 100644 index 00000000..768b2c21 --- /dev/null +++ b/qqq-sample-project/src/main/resources/site/public/index.html @@ -0,0 +1,30 @@ + + + + +

Welcome to the public site

+ + + + diff --git a/qqq-sample-project/src/main/resources/static-site/hello.txt b/qqq-sample-project/src/main/resources/static-site/hello.txt new file mode 100644 index 00000000..e5b8f9ce --- /dev/null +++ b/qqq-sample-project/src/main/resources/static-site/hello.txt @@ -0,0 +1 @@ +World! \ No newline at end of file diff --git a/qqq-sample-project/src/main/resources/static-site/index.html b/qqq-sample-project/src/main/resources/static-site/index.html new file mode 100644 index 00000000..611391a3 --- /dev/null +++ b/qqq-sample-project/src/main/resources/static-site/index.html @@ -0,0 +1,22 @@ + + +hello world \ No newline at end of file diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java index e342f586..e3cd2762 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java @@ -24,6 +24,7 @@ package com.kingsrook.sampleapp; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider; import org.junit.jupiter.api.Test; @@ -43,8 +44,9 @@ class SampleCliTest @Test void testExitSuccess() throws QException { - QContext.init(SampleMetaDataProvider.defineInstance(), new QSession()); - int exitCode = new SampleCli().run(new String[] { "--meta-data" }); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); + QContext.init(qInstance, new QSession()); + int exitCode = new SampleCli().run(qInstance, new String[] { "--meta-data" }); assertEquals(0, exitCode); } @@ -56,8 +58,9 @@ class SampleCliTest @Test void testNotExitSuccess() throws QException { - QContext.init(SampleMetaDataProvider.defineInstance(), new QSession()); - int exitCode = new SampleCli().run(new String[] { "asdfasdf" }); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); + QContext.init(qInstance, new QSession()); + int exitCode = new SampleCli().run(qInstance, new String[] { "asdfasdf" }); assertNotEquals(0, exitCode); } diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java index e2b488ca..8f584ee9 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java @@ -75,7 +75,7 @@ public class SampleMetaDataProviderTest void beforeEach() throws Exception { primeTestDatabase("prime-test-database.sql"); - QContext.init(SampleMetaDataProvider.defineInstance(), new QSession()); + QContext.init(SampleMetaDataProvider.defineTestInstance(), new QSession()); } @@ -190,7 +190,7 @@ public class SampleMetaDataProviderTest @Test public void testGreetProcess() throws Exception { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); QTableMetaData personTable = SampleMetaDataProvider.defineTablePerson(); RunProcessInput request = new RunProcessInput(); request.setProcessName(SampleMetaDataProvider.PROCESS_NAME_GREET); @@ -216,7 +216,7 @@ public class SampleMetaDataProviderTest @Test public void testThrowProcess() throws Exception { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); RunProcessInput request = new RunProcessInput(); request.setProcessName(SampleMetaDataProvider.PROCESS_NAME_SIMPLE_THROW); request.addValue(SampleMetaDataProvider.ThrowerStep.FIELD_SLEEP_MILLIS, 10); diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java index 4b9af90c..f5599a79 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java @@ -47,7 +47,7 @@ class RenderAllWidgetsTest @Test void test() throws QException { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); QContext.init(qInstance, new QSession()); //////////////////////////////////////////////////////////////// diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java index 4de7c550..5dc4245e 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java @@ -82,7 +82,7 @@ class ClonePeopleTransformStepTest @Test void testProcessStep() throws QException { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); QContext.init(qInstance, new QSession()); QueryInput queryInput = new QueryInput();