From e53d559b29c00fbd944ac503860045cbd8eb4a44 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 6 Oct 2022 19:44:27 -0500 Subject: [PATCH] SPRINT-12: added process step class validation, added noop transform step --- .../core/instances/QInstanceValidator.java | 80 ++++++++-- .../NoopTransformStep.java | 87 +++++++++++ .../instances/QInstanceValidatorTest.java | 143 +++++++++++++++++- 3 files changed, 294 insertions(+), 16 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamedwithfrontend/NoopTransformStep.java 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; + } + } } +