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 ed206b5e..262b6a42 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
@@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.instances;
import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -30,17 +31,21 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
+import java.util.stream.Stream;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
@@ -461,18 +466,48 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
- private Object getInstanceOfCodeReference(String prefix, Class> customizerClass)
+ private Object getInstanceOfCodeReference(String prefix, Class> clazz)
{
- Object customizerInstance = null;
+ Object instance = null;
try
{
- customizerInstance = customizerClass.getConstructor().newInstance();
+ instance = clazz.getConstructor().newInstance();
}
catch(InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e)
{
- errors.add(prefix + "Instance of CodeReference could not be created: " + e);
+ prefix += "Instance of " + clazz.getSimpleName() + " could not be created";
+ if(Modifier.isAbstract(clazz.getModifiers()))
+ {
+ errors.add(prefix + " because it is abstract");
+ }
+ else if(Modifier.isInterface(clazz.getModifiers()))
+ {
+ errors.add(prefix + " because it is an interface");
+ }
+ else if(!Modifier.isPublic(clazz.getModifiers()))
+ {
+ errors.add(prefix + " because it is not public");
+ }
+ else
+ {
+ //////////////////////////////////
+ // check for no-arg constructor //
+ //////////////////////////////////
+ boolean hasNoArgConstructor = Stream.of(clazz.getConstructors()).anyMatch(c -> c.getParameterCount() == 0);
+ if(!hasNoArgConstructor)
+ {
+ errors.add(prefix + " because it does not have a parameterless constructor");
+ }
+ else
+ {
+ //////////////////////////////////////////
+ // otherwise, just append the exception //
+ //////////////////////////////////////////
+ errors.add(prefix + ": " + e);
+ }
+ }
}
- return customizerInstance;
+ return instance;
}
@@ -579,6 +614,23 @@ public class QInstanceValidator
{
assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName);
index++;
+
+ ////////////////////////////////////////////
+ // validate instantiation of step classes //
+ ////////////////////////////////////////////
+ if(step instanceof QBackendStepMetaData backendStepMetaData)
+ {
+ if(backendStepMetaData.getInputMetaData() != null && CollectionUtils.nullSafeHasContents(backendStepMetaData.getInputMetaData().getFieldList()))
+ {
+ for(QFieldMetaData fieldMetaData : backendStepMetaData.getInputMetaData().getFieldList())
+ {
+ if(fieldMetaData.getDefaultValue() != null && fieldMetaData.getDefaultValue() instanceof QCodeReference codeReference)
+ {
+ validateSimpleCodeReference("Process " + processName + " backend step code reference: ", codeReference, BackendStep.class);
+ }
+ }
+ }
+ }
}
}
});
@@ -715,20 +767,20 @@ public class QInstanceValidator
///////////////////////////////////////
// make sure the class can be loaded //
///////////////////////////////////////
- Class> customizerClass = getClassForCodeReference(codeReference, prefix);
- if(customizerClass != null)
+ Class> clazz = getClassForCodeReference(codeReference, prefix);
+ if(clazz != null)
{
//////////////////////////////////////////////////
// make sure the customizer can be instantiated //
//////////////////////////////////////////////////
- Object customizerInstance = getInstanceOfCodeReference(prefix, customizerClass);
+ Object classInstance = getInstanceOfCodeReference(prefix, clazz);
////////////////////////////////////////////////////////////////////////
// make sure the customizer instance can be cast to the expected type //
////////////////////////////////////////////////////////////////////////
- if(customizerInstance != null)
+ if(classInstance != null)
{
- getCastedObject(prefix, expectedClass, customizerInstance);
+ getCastedObject(prefix, expectedClass, classInstance);
}
}
}
@@ -741,16 +793,16 @@ public class QInstanceValidator
*******************************************************************************/
private Class> getClassForCodeReference(QCodeReference codeReference, String prefix)
{
- Class> customizerClass = null;
+ Class> clazz = null;
try
{
- customizerClass = Class.forName(codeReference.getName());
+ clazz = Class.forName(codeReference.getName());
}
catch(ClassNotFoundException e)
{
- errors.add(prefix + "Class for CodeReference could not be found.");
+ errors.add(prefix + "Class for " + codeReference.getName() + " could not be found.");
}
- return customizerClass;
+ return clazz;
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/NoopTransformStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/NoopTransformStep.java
new file mode 100644
index 00000000..eebacd18
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/NoopTransformStep.java
@@ -0,0 +1,87 @@
+/*
+ * 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.processes.implementations.etl.streamedwithfrontend;
+
+
+import java.util.ArrayList;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
+import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.Status;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+
+
+/*******************************************************************************
+ ** Implementation of a TransformStep - it does nothing other than take input records
+ ** and sets them in the output
+ *******************************************************************************/
+public class NoopTransformStep extends AbstractTransformStep
+{
+ private final String okSummarySuffix = " successfully processed.";
+ private final ProcessSummaryLine okSummary = new ProcessSummaryLine(Status.OK)
+ .withSingularFutureMessage("can be" + okSummarySuffix)
+ .withPluralFutureMessage("can be" + okSummarySuffix)
+ .withSingularPastMessage("has been" + okSummarySuffix)
+ .withPluralPastMessage("have been" + okSummarySuffix);
+
+
+
+ /*******************************************************************************
+ ** getProcessSummary
+ *
+ *******************************************************************************/
+ @Override
+ public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
+ {
+ ArrayList rs = new ArrayList<>();
+ okSummary.addSelfToListIfAnyCount(rs);
+ return (rs);
+ }
+
+
+
+ /*******************************************************************************
+ ** run
+ *
+ *******************************************************************************/
+ @Override
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ ////////////////////////////////
+ // return if no input records //
+ ////////////////////////////////
+ if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
+ {
+ return;
+ }
+
+ for(QRecord qRecord : runBackendStepInput.getRecords())
+ {
+ okSummary.incrementCountAndAddPrimaryKey(qRecord.getValue(runBackendStepInput.getTable().getPrimaryKeyField()));
+ runBackendStepOutput.getRecords().add(qRecord);
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
index 44d4700c..35567ae4 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
@@ -29,7 +29,12 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
+import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
+import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
+import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@@ -45,10 +50,15 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
+import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
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.model.metadata.tables.automation.TableAutomationAction;
+import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
+import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
+import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaDeleteStep;
+import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -230,6 +240,75 @@ class QInstanceValidatorTest
+ /*******************************************************************************
+ ** Test that a process with a step that is a private class fails
+ **
+ *******************************************************************************/
+ @Test
+ public void test_validateProcessWithPrivateStep()
+ {
+ QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
+ ExtractViaQueryStep.class,
+ TestPrivateClass.class,
+ LoadViaDeleteStep.class,
+ new HashMap<>()
+ );
+ process.setName("testProcess");
+ process.setLabel("Test Process");
+ process.setTableName(TestUtils.defineTablePerson().getName());
+
+ assertValidationFailureReasons((qInstance) -> qInstance.addProcess(process),
+ "is not public");
+ }
+
+
+
+ /*******************************************************************************
+ ** Test that a process with a step that does not have a no-args constructor fails
+ **
+ *******************************************************************************/
+ @Test
+ public void test_validateProcessWithNoArgsConstructorStep()
+ {
+ QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
+ ExtractViaQueryStep.class,
+ TestNoArgsConstructorClass.class,
+ LoadViaDeleteStep.class,
+ new HashMap<>()
+ );
+ process.setName("testProcess");
+ process.setLabel("Test Process");
+ process.setTableName(TestUtils.defineTablePerson().getName());
+
+ assertValidationFailureReasons((qInstance) -> qInstance.addProcess(process),
+ "parameterless constructor");
+ }
+
+
+
+ /*******************************************************************************
+ ** Test that a process with a step that is an abstract class fails
+ **
+ *******************************************************************************/
+ @Test
+ public void test_validateProcessWithAbstractStep()
+ {
+ QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
+ ExtractViaQueryStep.class,
+ TestAbstractClass.class,
+ LoadViaDeleteStep.class,
+ new HashMap<>()
+ );
+ process.setName("testProcess");
+ process.setLabel("Test Process");
+ process.setTableName(TestUtils.defineTablePerson().getName());
+
+ assertValidationFailureReasons((qInstance) -> qInstance.addProcess(process),
+ "because it is abstract");
+ }
+
+
+
/*******************************************************************************
** Test that a process with no steps fails
**
@@ -297,10 +376,10 @@ class QInstanceValidatorTest
"missing a code type");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference("Test", QCodeType.JAVA, QCodeUsage.CUSTOMIZER)),
- "Class for CodeReference could not be found");
+ "Class for Test could not be found");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerWithNoVoidConstructor.class, QCodeUsage.CUSTOMIZER)),
- "Instance of CodeReference could not be created");
+ "Instance of " + CustomizerWithNoVoidConstructor.class.getSimpleName() + " could not be created");
assertValidationFailureReasons((qInstance) -> qInstance.getTable("person").withCustomizer(TableCustomizers.POST_QUERY_RECORD.getRole(), new QCodeReference(CustomizerThatIsNotAFunction.class, QCodeUsage.CUSTOMIZER)),
"CodeReference is not of the expected type");
@@ -1066,4 +1145,64 @@ class QInstanceValidatorTest
.withFailMessage("Expected any of:\n%s\nTo match: [%s]", e.getReasons(), reason)
.anyMatch(s -> s.contains(reason));
}
+
+
+
+ ///////////////////////////////////////////////
+ // test classes for validating process steps //
+ ///////////////////////////////////////////////
+ public abstract class TestAbstractClass extends AbstractTransformStep implements BackendStep
+ {
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ }
+ }
+
+
+
+ ///////////////////////////////////////////////
+ // //
+ ///////////////////////////////////////////////
+ private class TestPrivateClass extends AbstractTransformStep implements BackendStep
+ {
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ }
+
+
+
+ @Override
+ public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
+ {
+ return null;
+ }
+ }
+
+
+
+ ///////////////////////////////////////////////
+ // //
+ ///////////////////////////////////////////////
+ public class TestNoArgsConstructorClass extends AbstractTransformStep implements BackendStep
+ {
+ public TestNoArgsConstructorClass(int i)
+ {
+
+ }
+
+
+
+ public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
+ {
+ }
+
+
+
+ @Override
+ public ArrayList getProcessSummary(RunBackendStepOutput runBackendStepOutput, boolean isForResultScreen)
+ {
+ return null;
+ }
+ }
}
+