From 909798436e253a39164f31e27e935d0d101bc9e7 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 3 May 2022 19:12:58 -0500 Subject: [PATCH] Checkpoint - version of processes that pass a unit test - basic version of stateProviderInterface with memory & temp-file implementations --- .../core/actions/RunFunctionAction.java | 27 +++--- .../core/actions/RunProcessAction.java | 56 ++++++++++- .../backend/core/interfaces/FunctionBody.java | 2 +- .../interfaces/mock/MockFunctionBody.java | 7 +- .../model/actions/processes/ProcessState.java | 67 +++++++++++++ .../actions/processes/RunFunctionRequest.java | 53 +++++++---- .../actions/processes/RunFunctionResult.java | 45 ++++++--- .../actions/processes/RunProcessResult.java | 35 ++++--- .../qqq/backend/core/model/data/QRecord.java | 20 ++-- .../processes/QFunctionInputMetaData.java | 2 +- .../processes/QRecordListMetaData.java | 11 --- .../core/modules/mock/MockQueryAction.java | 20 +++- .../backend/core/state/AbstractStateKey.java | 35 +++++++ .../core/state/InMemoryStateProvider.java | 75 +++++++++++++++ .../core/state/StateProviderInterface.java | 26 +++++ .../core/state/TempFileStateProvider.java | 88 +++++++++++++++++ .../qqq/backend/core/state/UUIDStateKey.java | 95 +++++++++++++++++++ .../backend/core/actions/RunProcessTest.java | 89 +++++++++++++++++ .../person/addtopeoplesage/AddAge.java | 33 +++++++ .../addtopeoplesage/GetAgeStatistics.java | 41 ++++++++ .../qqq/backend/core/utils/TestUtils.java | 48 ++++++++++ 21 files changed, 785 insertions(+), 90 deletions(-) create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/state/AbstractStateKey.java create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/state/InMemoryStateProvider.java create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/state/StateProviderInterface.java create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/state/TempFileStateProvider.java create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/state/UUIDStateKey.java create mode 100644 src/test/java/com/kingsrook/qqq/backend/core/actions/RunProcessTest.java create mode 100644 src/test/java/com/kingsrook/qqq/backend/core/actions/processes/person/addtopeoplesage/AddAge.java create mode 100644 src/test/java/com/kingsrook/qqq/backend/core/actions/processes/person/addtopeoplesage/GetAgeStatistics.java diff --git a/src/main/java/com/kingsrook/qqq/backend/core/actions/RunFunctionAction.java b/src/main/java/com/kingsrook/qqq/backend/core/actions/RunFunctionAction.java index 4cf0c620..c340ed81 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/actions/RunFunctionAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/actions/RunFunctionAction.java @@ -21,6 +21,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* @@ -61,9 +62,7 @@ public class RunFunctionAction //////////////////////////////////////////////////////////////////// // load and run the user-defined code that actually does the work // //////////////////////////////////////////////////////////////////// - RunFunctionResult runFunctionResult = runFunctionBodyCode(function.getCode(), runFunctionRequest); - - return (runFunctionResult); + return (runFunctionBodyCode(function.getCode(), runFunctionRequest)); } @@ -88,11 +87,14 @@ public class RunFunctionAction } } - Map fieldValues = runFunctionRequest.getCallback().getFieldValues(fieldsToGet); - for(Map.Entry entry : fieldValues.entrySet()) + if(!fieldsToGet.isEmpty()) { - runFunctionRequest.addValue(entry.getKey(), entry.getValue()); - // todo - check to make sure got values back? + Map fieldValues = runFunctionRequest.getCallback().getFieldValues(fieldsToGet); + for(Map.Entry entry : fieldValues.entrySet()) + { + runFunctionRequest.addValue(entry.getKey(), entry.getValue()); + // todo - check to make sure got values back? + } } } @@ -108,7 +110,7 @@ public class RunFunctionAction QRecordListMetaData recordListMetaData = inputMetaData.getRecordListMetaData(); if(recordListMetaData != null) { - if(runFunctionRequest.getRecords() == null) + if(CollectionUtils.nullSafeIsEmpty(runFunctionRequest.getRecords())) { QueryRequest queryRequest = new QueryRequest(runFunctionRequest.getInstance()); queryRequest.setSession(runFunctionRequest.getSession()); @@ -133,18 +135,19 @@ public class RunFunctionAction *******************************************************************************/ private RunFunctionResult runFunctionBodyCode(QCodeReference code, RunFunctionRequest runFunctionRequest) { - RunFunctionResult runFunctionResult; + RunFunctionResult runFunctionResult = new RunFunctionResult(); try { + runFunctionResult.seedFromRequest(runFunctionRequest); + Class codeClass = Class.forName(code.getName()); Object codeObject = codeClass.getConstructor().newInstance(); - if(!(codeObject instanceof FunctionBody)) + if(!(codeObject instanceof FunctionBody functionBodyCodeObject)) { throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of FunctionBody")); } - FunctionBody functionBodyCodeObject = (FunctionBody) codeObject; - runFunctionResult = functionBodyCodeObject.run(runFunctionRequest); + functionBodyCodeObject.run(runFunctionRequest, runFunctionResult); } catch(Exception e) { diff --git a/src/main/java/com/kingsrook/qqq/backend/core/actions/RunProcessAction.java b/src/main/java/com/kingsrook/qqq/backend/core/actions/RunProcessAction.java index 80874fa5..07f21297 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/actions/RunProcessAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/actions/RunProcessAction.java @@ -7,12 +7,16 @@ package com.kingsrook.qqq.backend.core.actions; import java.util.List; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest; import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionResult; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.state.StateProviderInterface; +import com.kingsrook.qqq.backend.core.state.TempFileStateProvider; +import com.kingsrook.qqq.backend.core.state.UUIDStateKey; /******************************************************************************* @@ -40,23 +44,67 @@ public class RunProcessAction RunProcessResult runProcessResult = new RunProcessResult(); + UUIDStateKey stateKey = new UUIDStateKey(); + RunFunctionResult lastFunctionResult = null; + // todo - custom routing? List functionList = process.getFunctionList(); for(QFunctionMetaData function : functionList) { RunFunctionRequest runFunctionRequest = new RunFunctionRequest(runProcessRequest.getInstance()); + + if(lastFunctionResult != null) + { + loadState(stateKey, runFunctionRequest); + } + runFunctionRequest.setProcessName(process.getName()); runFunctionRequest.setFunctionName(function.getName()); - // todo - how does this work again? runFunctionRequest.setCallback(?); - RunFunctionResult functionResult = new RunFunctionAction().execute(runFunctionRequest); - if(functionResult.getError() != null) + runFunctionRequest.setSession(runProcessRequest.getSession()); + runFunctionRequest.setCallback(runProcessRequest.getCallback()); + lastFunctionResult = new RunFunctionAction().execute(runFunctionRequest); + if(lastFunctionResult.getError() != null) { - runProcessResult.setError(functionResult.getError()); + runProcessResult.setError(lastFunctionResult.getError()); break; } + + storeState(stateKey, lastFunctionResult); + } + + if(lastFunctionResult != null) + { + runProcessResult.seedFromLastFunctionResult(lastFunctionResult); } return (runProcessResult); } + + + /******************************************************************************* + ** + *******************************************************************************/ + private StateProviderInterface getStateProvider() + { + // TODO - read this from somewhere in meta data eh? + // return InMemoryStateProvider.getInstance(); + return TempFileStateProvider.getInstance(); + } + + + + private void storeState(UUIDStateKey stateKey, RunFunctionResult runFunctionResult) + { + getStateProvider().put(stateKey, runFunctionResult.getProcessState()); + } + + + + private void loadState(UUIDStateKey stateKey, RunFunctionRequest runFunctionRequest) + { + ProcessState processState = getStateProvider().get(ProcessState.class, stateKey); + runFunctionRequest.seedFromProcessState(processState); + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/interfaces/FunctionBody.java b/src/main/java/com/kingsrook/qqq/backend/core/interfaces/FunctionBody.java index 8e89251c..14dea315 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/interfaces/FunctionBody.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/interfaces/FunctionBody.java @@ -17,5 +17,5 @@ public interface FunctionBody /******************************************************************************* ** TODO - document! *******************************************************************************/ - RunFunctionResult run(RunFunctionRequest runFunctionRequest); + void run(RunFunctionRequest runFunctionRequest, RunFunctionResult runFunctionResult); } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/interfaces/mock/MockFunctionBody.java b/src/main/java/com/kingsrook/qqq/backend/core/interfaces/mock/MockFunctionBody.java index a0364d68..3f3152af 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/interfaces/mock/MockFunctionBody.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/interfaces/mock/MockFunctionBody.java @@ -18,16 +18,11 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionResult; public class MockFunctionBody implements FunctionBody { @Override - public RunFunctionResult run(RunFunctionRequest runFunctionRequest) + public void run(RunFunctionRequest runFunctionRequest, RunFunctionResult runFunctionResult) { - RunFunctionResult runFunctionResult = new RunFunctionResult(); - runFunctionResult.setRecords(runFunctionRequest.getRecords()); - runFunctionResult.getRecords().forEach(r -> r.setValue("mockValue", "Ha ha!")); runFunctionResult.setValues(runFunctionRequest.getValues()); runFunctionResult.addValue("mockValue", "You so silly"); - - return (runFunctionResult); } } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java new file mode 100644 index 00000000..f42e4242 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/ProcessState.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2021-2022. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.backend.core.model.actions.processes; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ProcessState implements Serializable +{ + private List records = new ArrayList<>(); + private Map values = new HashMap<>(); + + + + /******************************************************************************* + ** Getter for records + ** + *******************************************************************************/ + public List getRecords() + { + return records; + } + + + + /******************************************************************************* + ** Setter for records + ** + *******************************************************************************/ + public void setRecords(List records) + { + this.records = records; + } + + + + /******************************************************************************* + ** Getter for values + ** + *******************************************************************************/ + public Map getValues() + { + return values; + } + + + + /******************************************************************************* + ** Setter for values + ** + *******************************************************************************/ + public void setValues(Map values) + { + this.values = values; + } +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunFunctionRequest.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunFunctionRequest.java index a9eb94e0..ab67e10b 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunFunctionRequest.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunFunctionRequest.java @@ -6,7 +6,6 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; -import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback; @@ -22,10 +21,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData *******************************************************************************/ public class RunFunctionRequest extends AbstractQRequest { + private ProcessState processState; private String processName; private String functionName; - private List records; - private Map values; private QProcessCallback callback; @@ -35,6 +33,7 @@ public class RunFunctionRequest extends AbstractQRequest *******************************************************************************/ public RunFunctionRequest() { + processState = new ProcessState(); } @@ -45,6 +44,19 @@ public class RunFunctionRequest extends AbstractQRequest public RunFunctionRequest(QInstance instance) { super(instance); + processState = new ProcessState(); + } + + + + /******************************************************************************* + ** e.g., for steps after the first step in a process, seed the data in a run + ** function request from a process state. + ** + *******************************************************************************/ + public void seedFromProcessState(ProcessState processState) + { + this.processState = processState; } @@ -133,7 +145,7 @@ public class RunFunctionRequest extends AbstractQRequest *******************************************************************************/ public List getRecords() { - return records; + return processState.getRecords(); } @@ -144,7 +156,7 @@ public class RunFunctionRequest extends AbstractQRequest *******************************************************************************/ public void setRecords(List records) { - this.records = records; + this.processState.setRecords(records); } @@ -155,7 +167,7 @@ public class RunFunctionRequest extends AbstractQRequest *******************************************************************************/ public RunFunctionRequest withRecords(List records) { - this.records = records; + this.processState.setRecords(records); return (this); } @@ -167,7 +179,7 @@ public class RunFunctionRequest extends AbstractQRequest *******************************************************************************/ public Map getValues() { - return values; + return processState.getValues(); } @@ -178,7 +190,7 @@ public class RunFunctionRequest extends AbstractQRequest *******************************************************************************/ public void setValues(Map values) { - this.values = values; + this.processState.setValues(values); } @@ -189,7 +201,7 @@ public class RunFunctionRequest extends AbstractQRequest *******************************************************************************/ public RunFunctionRequest withValues(Map values) { - this.values = values; + this.processState.setValues(values); return (this); } @@ -201,11 +213,7 @@ public class RunFunctionRequest extends AbstractQRequest *******************************************************************************/ public RunFunctionRequest addValue(String fieldName, Serializable value) { - if(this.values == null) - { - this.values = new HashMap<>(); - } - this.values.put(fieldName, value); + this.processState.getValues().put(fieldName, value); return (this); } @@ -251,11 +259,7 @@ public class RunFunctionRequest extends AbstractQRequest *******************************************************************************/ public Serializable getValue(String fieldName) { - if(values == null) - { - return (null); - } - return (values.get(fieldName)); + return (processState.getValues().get(fieldName)); } @@ -280,4 +284,15 @@ public class RunFunctionRequest extends AbstractQRequest return ((Integer) getValue(fieldName)); } + + + /******************************************************************************* + ** Accessor for processState - protected, because we generally want to access + ** its members through wrapper methods, we think + ** + *******************************************************************************/ + protected ProcessState getProcessState() + { + return processState; + } } \ No newline at end of file diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunFunctionResult.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunFunctionResult.java index 7bdb1329..f0d11ee8 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunFunctionResult.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunFunctionResult.java @@ -6,7 +6,6 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; -import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.model.actions.AbstractQResult; @@ -19,8 +18,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; *******************************************************************************/ public class RunFunctionResult extends AbstractQResult { - private List records; - private Map values; + private ProcessState processState; private String error; @@ -30,6 +28,18 @@ public class RunFunctionResult extends AbstractQResult *******************************************************************************/ public RunFunctionResult() { + this.processState = new ProcessState(); + } + + + + /******************************************************************************* + ** e.g., populate the process state (records, values) in this result object. + ** + *******************************************************************************/ + public void seedFromRequest(RunFunctionRequest runFunctionRequest) + { + this.processState = runFunctionRequest.getProcessState(); } @@ -40,7 +50,7 @@ public class RunFunctionResult extends AbstractQResult *******************************************************************************/ public List getRecords() { - return records; + return processState.getRecords(); } @@ -51,7 +61,7 @@ public class RunFunctionResult extends AbstractQResult *******************************************************************************/ public void setRecords(List records) { - this.records = records; + this.processState.setRecords(records); } @@ -62,7 +72,7 @@ public class RunFunctionResult extends AbstractQResult *******************************************************************************/ public RunFunctionResult withRecords(List records) { - this.records = records; + this.processState.setRecords(records); return (this); } @@ -74,7 +84,7 @@ public class RunFunctionResult extends AbstractQResult *******************************************************************************/ public Map getValues() { - return values; + return processState.getValues(); } @@ -85,7 +95,7 @@ public class RunFunctionResult extends AbstractQResult *******************************************************************************/ public void setValues(Map values) { - this.values = values; + this.processState.setValues(values); } @@ -96,7 +106,7 @@ public class RunFunctionResult extends AbstractQResult *******************************************************************************/ public RunFunctionResult withValues(Map values) { - this.values = values; + this.processState.setValues(values); return (this); } @@ -108,11 +118,7 @@ public class RunFunctionResult extends AbstractQResult *******************************************************************************/ public RunFunctionResult addValue(String fieldName, Serializable value) { - if(this.values == null) - { - this.values = new HashMap<>(); - } - this.values.put(fieldName, value); + this.processState.getValues().put(fieldName, value); return (this); } @@ -137,4 +143,15 @@ public class RunFunctionResult extends AbstractQResult { this.error = error; } + + + + /******************************************************************************* + ** Accessor for processState + ** + *******************************************************************************/ + public ProcessState getProcessState() + { + return processState; + } } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessResult.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessResult.java index 5bab2fb9..4ecab46c 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessResult.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/processes/RunProcessResult.java @@ -6,7 +6,6 @@ package com.kingsrook.qqq.backend.core.model.actions.processes; import java.io.Serializable; -import java.util.HashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.model.actions.AbstractQResult; @@ -19,8 +18,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; *******************************************************************************/ public class RunProcessResult extends AbstractQResult { - private List records; - private Map values; + private ProcessState processState; private String error; @@ -30,6 +28,19 @@ public class RunProcessResult extends AbstractQResult *******************************************************************************/ public RunProcessResult() { + processState = new ProcessState(); + } + + + + /******************************************************************************* + ** e.g., populate the process state (records, values) in this result object from + ** the final function result + ** + *******************************************************************************/ + public void seedFromLastFunctionResult(RunFunctionResult runFunctionResult) + { + this.processState = runFunctionResult.getProcessState(); } @@ -40,7 +51,7 @@ public class RunProcessResult extends AbstractQResult *******************************************************************************/ public List getRecords() { - return records; + return processState.getRecords(); } @@ -51,7 +62,7 @@ public class RunProcessResult extends AbstractQResult *******************************************************************************/ public void setRecords(List records) { - this.records = records; + this.processState.setRecords(records); } @@ -62,7 +73,7 @@ public class RunProcessResult extends AbstractQResult *******************************************************************************/ public RunProcessResult withRecords(List records) { - this.records = records; + this.processState.setRecords(records); return (this); } @@ -74,7 +85,7 @@ public class RunProcessResult extends AbstractQResult *******************************************************************************/ public Map getValues() { - return values; + return processState.getValues(); } @@ -85,7 +96,7 @@ public class RunProcessResult extends AbstractQResult *******************************************************************************/ public void setValues(Map values) { - this.values = values; + this.processState.setValues(values); } @@ -96,7 +107,7 @@ public class RunProcessResult extends AbstractQResult *******************************************************************************/ public RunProcessResult withValues(Map values) { - this.values = values; + this.processState.setValues(values); return (this); } @@ -108,11 +119,7 @@ public class RunProcessResult extends AbstractQResult *******************************************************************************/ public RunProcessResult addValue(String fieldName, Serializable value) { - if(this.values == null) - { - this.values = new HashMap<>(); - } - this.values.put(fieldName, value); + this.processState.getValues().put(fieldName, value); return (this); } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index b3f92f7c..ff38d882 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -6,6 +6,7 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; +import java.time.LocalDate; import java.util.LinkedHashMap; import java.util.Map; @@ -19,7 +20,7 @@ import java.util.Map; * "Display values" (e.g., labels for possible values, or formatted numbers * (e.g., quantities with commas)) are in the displayValues map. *******************************************************************************/ -public class QRecord +public class QRecord implements Serializable { private String tableName; //x private Serializable primaryKey; @@ -102,8 +103,6 @@ public class QRecord return (this); } - - //x /******************************************************************************* //x ** Getter for primaryKey //x ** @@ -113,8 +112,6 @@ public class QRecord //x return primaryKey; //x } - - //x /******************************************************************************* //x ** Setter for primaryKey //x ** @@ -124,8 +121,6 @@ public class QRecord //x this.primaryKey = primaryKey; //x } - - //x /******************************************************************************* //x ** Setter for primaryKey //x ** @@ -224,4 +219,15 @@ public class QRecord return ((Integer) values.get(fieldName)); } + + + /******************************************************************************* + ** Getter for a single field's value + ** + *******************************************************************************/ + public LocalDate getValueDate(String fieldName) + { + return ((LocalDate) values.get(fieldName)); + } + } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFunctionInputMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFunctionInputMetaData.java index 1c17e373..3095130c 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFunctionInputMetaData.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QFunctionInputMetaData.java @@ -17,7 +17,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; public class QFunctionInputMetaData { private QRecordListMetaData recordListMetaData; - private List fieldList; + private List fieldList = new ArrayList<>(); diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QRecordListMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QRecordListMetaData.java index c28c2bd1..eb1715c8 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QRecordListMetaData.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QRecordListMetaData.java @@ -96,17 +96,6 @@ public class QRecordListMetaData - /******************************************************************************* - ** - *******************************************************************************/ - public QRecordListMetaData withFields(Map fields) - { - this.fields = fields; - return (this); - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/mock/MockQueryAction.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/mock/MockQueryAction.java index 453d4317..7ea54f7e 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/mock/MockQueryAction.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/mock/MockQueryAction.java @@ -5,6 +5,11 @@ package com.kingsrook.qqq.backend.core.modules.mock; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -41,7 +46,20 @@ public class MockQueryAction implements QueryInterface for(String field : table.getFields().keySet()) { - record.setValue(field, "1"); + Serializable value = switch (table.getField(field).getType()) + { + case STRING -> "Foo"; + case INTEGER -> 42; + case DECIMAL -> new BigDecimal("3.14159"); + case DATE -> LocalDate.of(1970, Month.JANUARY, 1); + case DATE_TIME -> LocalDateTime.of(1970, Month.JANUARY, 1, 0, 0); + case TEXT -> "Four score and seven years ago..."; + case HTML -> "BOLD"; + case PASSWORD -> "abc***234"; + default -> throw new IllegalStateException("Unexpected value: " + table.getField(field).getType()); + }; + + record.setValue(field, value); } return rs; diff --git a/src/main/java/com/kingsrook/qqq/backend/core/state/AbstractStateKey.java b/src/main/java/com/kingsrook/qqq/backend/core/state/AbstractStateKey.java new file mode 100644 index 00000000..d19320db --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/state/AbstractStateKey.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2021-2022. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.backend.core.state; + + +/******************************************************************************* + ** + *******************************************************************************/ +public abstract class AbstractStateKey +{ + + /******************************************************************************* + ** Require all state keys to implement the equals method + * + *******************************************************************************/ + @Override + public abstract boolean equals(Object that); + + /******************************************************************************* + ** Require all state keys to implement the hashCode method + * + *******************************************************************************/ + @Override + public abstract int hashCode(); + + /******************************************************************************* + ** Require all state keys to implement the toString method + * + *******************************************************************************/ + @Override + public abstract String toString(); + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/state/InMemoryStateProvider.java b/src/main/java/com/kingsrook/qqq/backend/core/state/InMemoryStateProvider.java new file mode 100644 index 00000000..41343a3a --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/state/InMemoryStateProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2021-2022. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.backend.core.state; + + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + + +/******************************************************************************* + ** Singleton class that provides a (non-persistent!!) in-memory state provider. + *******************************************************************************/ +public class InMemoryStateProvider implements StateProviderInterface +{ + private static InMemoryStateProvider instance; + + private final Map map; + + + + /******************************************************************************* + ** Private constructor for singleton. + *******************************************************************************/ + private InMemoryStateProvider() + { + this.map = new HashMap<>(); + } + + + + /******************************************************************************* + ** Singleton accessor + *******************************************************************************/ + public static InMemoryStateProvider getInstance() + { + if(instance == null) + { + instance = new InMemoryStateProvider(); + } + return instance; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void put(AbstractStateKey key, T data) + { + map.put(key, data); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public T get(Class type, AbstractStateKey key) + { + try + { + return type.cast(map.get(key)); + } + catch(ClassCastException cce) + { + throw new RuntimeException("Stored state value could not be cast to desired type", cce); + } + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/state/StateProviderInterface.java b/src/main/java/com/kingsrook/qqq/backend/core/state/StateProviderInterface.java new file mode 100644 index 00000000..fb0cddf7 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/state/StateProviderInterface.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2021-2022. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.backend.core.state; + + +import java.io.Serializable; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface StateProviderInterface +{ + + /******************************************************************************* + ** Put a block of data, under a key, into the state store. + *******************************************************************************/ + void put(AbstractStateKey key, T data); + + /******************************************************************************* + ** Get a block of data, under a key, from the state store. + *******************************************************************************/ + T get(Class type, AbstractStateKey key); +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/state/TempFileStateProvider.java b/src/main/java/com/kingsrook/qqq/backend/core/state/TempFileStateProvider.java new file mode 100644 index 00000000..2ac5523e --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/state/TempFileStateProvider.java @@ -0,0 +1,88 @@ +/* + * Copyright © 2021-2022. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.backend.core.state; + + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import org.apache.commons.io.FileUtils; + + +/******************************************************************************* + ** Singleton class that provides a (non-persistent!!) in-memory state provider. + *******************************************************************************/ +public class TempFileStateProvider implements StateProviderInterface +{ + private static TempFileStateProvider instance; + + + + /******************************************************************************* + ** Private constructor for singleton. + *******************************************************************************/ + private TempFileStateProvider() + { + } + + + + /******************************************************************************* + ** Singleton accessor + *******************************************************************************/ + public static TempFileStateProvider getInstance() + { + if(instance == null) + { + instance = new TempFileStateProvider(); + } + return instance; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void put(AbstractStateKey key, T data) + { + try + { + String json = JsonUtils.toJson(data); + FileUtils.writeStringToFile(new File("/tmp/" + key.toString()), json); + } + catch(IOException e) + { + // todo better + e.printStackTrace(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public T get(Class type, AbstractStateKey key) + { + try + { + String json = FileUtils.readFileToString(new File("/tmp/" + key.toString())); + return JsonUtils.toObject(json, type); + } + catch(ClassCastException cce) + { + throw new RuntimeException("Stored state value could not be cast to desired type", cce); + } + catch(IOException ie) + { + throw new RuntimeException("Error loading state from file", ie); + } + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/state/UUIDStateKey.java b/src/main/java/com/kingsrook/qqq/backend/core/state/UUIDStateKey.java new file mode 100644 index 00000000..a2921640 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/state/UUIDStateKey.java @@ -0,0 +1,95 @@ +/* + * Copyright © 2021-2022. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.backend.core.state; + + +import java.util.Objects; +import java.util.UUID; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class UUIDStateKey extends AbstractStateKey +{ + private final UUID uuid; + + + + /******************************************************************************* + ** Default constructor - assigns a random UUID. + ** + *******************************************************************************/ + public UUIDStateKey() + { + uuid = UUID.randomUUID(); + } + + + + /******************************************************************************* + ** Constructor that lets you supply a UUID. + ** + *******************************************************************************/ + public UUIDStateKey(UUID uuid) + { + this.uuid = uuid; + } + + + + /******************************************************************************* + ** Getter for uuid + ** + *******************************************************************************/ + public UUID getUuid() + { + return uuid; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + + if(o == null || getClass() != o.getClass()) + { + return false; + } + + UUIDStateKey that = (UUIDStateKey) o; + return Objects.equals(uuid, that.uuid); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int hashCode() + { + return Objects.hash(uuid); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return uuid.toString(); + } +} diff --git a/src/test/java/com/kingsrook/qqq/backend/core/actions/RunProcessTest.java b/src/test/java/com/kingsrook/qqq/backend/core/actions/RunProcessTest.java new file mode 100644 index 00000000..8b0db8c2 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/core/actions/RunProcessTest.java @@ -0,0 +1,89 @@ +/* + * Copyright © 2021-2021. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.backend.core.actions; + + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult; +import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class RunProcessTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void test() throws QException + { + TestCallback callback = new TestCallback(); + RunProcessRequest request = new RunProcessRequest(TestUtils.defineInstance()); + request.setSession(TestUtils.getMockSession()); + request.setProcessName("addToPeoplesAge"); + request.setCallback(callback); + RunProcessResult result = new RunProcessAction().execute(request); + assertNotNull(result); + assertNull(result.getError()); + assertTrue(result.getRecords().stream().allMatch(r -> r.getValues().containsKey("age")), "records should have a value set by the process"); + assertTrue(result.getValues().containsKey("maxAge"), "process result object should have a value set by the first function in the process"); + assertTrue(result.getValues().containsKey("totalYearsAdded"), "process result object should have a value set by the second function in the process"); + assertTrue(callback.wasCalledForQueryFilter, "callback was used for query filter"); + assertTrue(callback.wasCalledForFieldValues, "callback was used for field values"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static class TestCallback implements QProcessCallback + { + private boolean wasCalledForQueryFilter = false; + private boolean wasCalledForFieldValues = false; + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QQueryFilter getQueryFilter() + { + wasCalledForQueryFilter = true; + return (new QQueryFilter()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Map getFieldValues(List fields) + { + wasCalledForFieldValues = true; + Map rs = new HashMap<>(); + if (fields.stream().anyMatch(f -> f.getName().equals("yearsToAdd"))) + { + rs.put("yearsToAdd", 42); + } + return (rs); + } + } +} diff --git a/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/person/addtopeoplesage/AddAge.java b/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/person/addtopeoplesage/AddAge.java new file mode 100644 index 00000000..def9fccf --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/person/addtopeoplesage/AddAge.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2021-2022. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage; + + +import com.kingsrook.qqq.backend.core.interfaces.FunctionBody; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionResult; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AddAge implements FunctionBody +{ + @Override + public void run(RunFunctionRequest runFunctionRequest, RunFunctionResult runFunctionResult) + { + int totalYearsAdded = 0; + Integer yearsToAdd = runFunctionRequest.getValueInteger("yearsToAdd"); + for(QRecord record : runFunctionRequest.getRecords()) + { + Integer age = record.getValueInteger("age"); + age += yearsToAdd; + totalYearsAdded += yearsToAdd; + record.setValue("age", age); + } + runFunctionResult.addValue("totalYearsAdded", totalYearsAdded); + } +} diff --git a/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/person/addtopeoplesage/GetAgeStatistics.java b/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/person/addtopeoplesage/GetAgeStatistics.java new file mode 100644 index 00000000..61f7ab8c --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/core/actions/processes/person/addtopeoplesage/GetAgeStatistics.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2021-2022. Kingsrook LLC . All Rights Reserved. + */ + +package com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage; + + +import java.time.LocalDate; +import java.time.Period; +import com.kingsrook.qqq.backend.core.interfaces.FunctionBody; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionRequest; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunFunctionResult; +import com.kingsrook.qqq.backend.core.model.data.QRecord; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class GetAgeStatistics implements FunctionBody +{ + @Override + public void run(RunFunctionRequest runFunctionRequest, RunFunctionResult runFunctionResult) + { + Integer min = null; + Integer max = null; + LocalDate now = LocalDate.now(); + for(QRecord record : runFunctionRequest.getRecords()) + { + LocalDate birthDate = record.getValueDate("birthDate"); + Period until = birthDate.until(now); + int age = until.getYears(); + record.setValue("age", age); + System.out.println(birthDate + " -> " + age); + min = (min == null || age < min) ? age : min; + max = (max == null || age > max) ? age : max; + } + + runFunctionResult.addValue("minAge", min); + runFunctionResult.addValue("maxAge", max); + } +} diff --git a/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 577d4dd3..616147ce 100644 --- a/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -46,6 +46,7 @@ public class TestUtils qInstance.addTable(defineTablePerson()); qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); qInstance.addProcess(defineProcessGreetPeople()); + qInstance.addProcess(defineProcessAddToPeoplesAge()); return (qInstance); } @@ -146,6 +147,53 @@ public class TestUtils + /******************************************************************************* + ** Define the "add to people's age" process + ** + ** Works on a list of rows from the person table. + ** - first function reports the current min & max age for all input rows. + ** - user is then prompted for how much they want to add to everyone. + ** - then the second function adds that value to their age, and shows the results. + *******************************************************************************/ + private static QProcessMetaData defineProcessAddToPeoplesAge() + { + return new QProcessMetaData() + .withName("addToPeoplesAge") + .withTableName("person") + .addFunction(new QFunctionMetaData() + .withName("getAgeStatistics") + .withCode(new QCodeReference() + .withName("com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics") + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.FUNCTION)) + .withInputData(new QFunctionInputMetaData() + .withRecordListMetaData(new QRecordListMetaData().withTableName("person"))) + .withOutputMetaData(new QFunctionOutputMetaData() + .withRecordListMetaData(new QRecordListMetaData() + .withTableName("person") + .addField(new QFieldMetaData("age", QFieldType.INTEGER))) + .withFieldList(List.of( + new QFieldMetaData("minAge", QFieldType.INTEGER), + new QFieldMetaData("maxAge", QFieldType.INTEGER)))) + .withOutputView(new QOutputView() + .withMessageField("outputMessage"))) // todo - wut? + .addFunction(new QFunctionMetaData() + .withName("addAge") + .withCode(new QCodeReference() + .withName("com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge") + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.FUNCTION)) + .withInputData(new QFunctionInputMetaData() + .withFieldList(List.of(new QFieldMetaData("yearsToAdd", QFieldType.INTEGER)))) + .withOutputMetaData(new QFunctionOutputMetaData() + .withRecordListMetaData(new QRecordListMetaData() + .withTableName("person") + .addField(new QFieldMetaData("newAge", QFieldType.INTEGER))))); + + } + + + /******************************************************************************* ** *******************************************************************************/