mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Merge branch 'release/0.1.0'
This commit is contained in:
@ -42,7 +42,7 @@ jobs:
|
|||||||
executor: java17
|
executor: java17
|
||||||
steps:
|
steps:
|
||||||
- run_maven:
|
- run_maven:
|
||||||
maven_subcommand: test
|
maven_subcommand: verify
|
||||||
- slack/notify:
|
- slack/notify:
|
||||||
event: fail
|
event: fail
|
||||||
|
|
||||||
|
@ -181,8 +181,8 @@
|
|||||||
</module>
|
</module>
|
||||||
-->
|
-->
|
||||||
<module name="OverloadMethodsDeclarationOrder"/>
|
<module name="OverloadMethodsDeclarationOrder"/>
|
||||||
<module name="VariableDeclarationUsageDistance"/>
|
|
||||||
<!--
|
<!--
|
||||||
|
<module name="VariableDeclarationUsageDistance"/>
|
||||||
<module name="CustomImportOrder">
|
<module name="CustomImportOrder">
|
||||||
<property name="sortImportsInGroupAlphabetically" value="true"/>
|
<property name="sortImportsInGroupAlphabetically" value="true"/>
|
||||||
<property name="separateLineBetweenGroups" value="true"/>
|
<property name="separateLineBetweenGroups" value="true"/>
|
||||||
|
89
pom.xml
89
pom.xml
@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<groupId>com.kingsrook.qqq</groupId>
|
<groupId>com.kingsrook.qqq</groupId>
|
||||||
<artifactId>qqq-middleware-javalin</artifactId>
|
<artifactId>qqq-middleware-javalin</artifactId>
|
||||||
<version>0.0.0</version>
|
<version>0.1.0</version>
|
||||||
|
|
||||||
<scm>
|
<scm>
|
||||||
<connection>scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git</connection>
|
<connection>scm:git:git@github.com:Kingsrook/qqq-middleware-javalin.git</connection>
|
||||||
@ -44,6 +44,8 @@
|
|||||||
<maven.compiler.target>17</maven.compiler.target>
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
|
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
|
||||||
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
|
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
|
||||||
|
<coverage.haltOnFailure>true</coverage.haltOnFailure>
|
||||||
|
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@ -51,12 +53,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.kingsrook.qqq</groupId>
|
<groupId>com.kingsrook.qqq</groupId>
|
||||||
<artifactId>qqq-backend-core</artifactId>
|
<artifactId>qqq-backend-core</artifactId>
|
||||||
<version>0.0.0</version>
|
<version>0.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.kingsrook.qqq</groupId>
|
<groupId>com.kingsrook.qqq</groupId>
|
||||||
<artifactId>qqq-backend-module-rdbms</artifactId>
|
<artifactId>qqq-backend-module-rdbms</artifactId>
|
||||||
<version>0.0.0</version>
|
<version>0.1.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
@ -126,6 +128,9 @@
|
|||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
<version>3.0.0-M5</version>
|
<version>3.0.0-M5</version>
|
||||||
|
<configuration>
|
||||||
|
<argLine>@{jaCoCoArgLine}</argLine>
|
||||||
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
@ -175,6 +180,84 @@
|
|||||||
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
|
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jacoco</groupId>
|
||||||
|
<artifactId>jacoco-maven-plugin</artifactId>
|
||||||
|
<version>0.8.8</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>pre-unit-test</id>
|
||||||
|
<goals>
|
||||||
|
<goal>prepare-agent</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<propertyName>jaCoCoArgLine</propertyName>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>unit-test-check</id>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<!-- Gives us the ability to pass a parameter to not fail due to coverage E.g. -Dcoverage.haltOnFailure=false -->
|
||||||
|
<haltOnFailure>${coverage.haltOnFailure}</haltOnFailure>
|
||||||
|
<rules>
|
||||||
|
<rule>
|
||||||
|
<element>BUNDLE</element>
|
||||||
|
<limits>
|
||||||
|
<limit>
|
||||||
|
<counter>INSTRUCTION</counter>
|
||||||
|
<value>COVEREDRATIO</value>
|
||||||
|
<minimum>${coverage.instructionCoveredRatioMinimum}</minimum>
|
||||||
|
</limit>
|
||||||
|
</limits>
|
||||||
|
</rule>
|
||||||
|
</rules>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>post-unit-test</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>report</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>exec-maven-plugin</artifactId>
|
||||||
|
<groupId>org.codehaus.mojo</groupId>
|
||||||
|
<version>3.0.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>test-coverage-summary</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>exec</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<executable>sh</executable>
|
||||||
|
<arguments>
|
||||||
|
<argument>-c</argument>
|
||||||
|
<argument>
|
||||||
|
<![CDATA[
|
||||||
|
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
|
||||||
|
xpath -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
|
||||||
|
echo
|
||||||
|
echo "Jacoco coverage summary report:"
|
||||||
|
echo " See also target/site/jacoco/index.html"
|
||||||
|
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
|
||||||
|
echo "------------------------------------------------------------"
|
||||||
|
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
|
||||||
|
rm /tmp/$$.headers /tmp/$$.values
|
||||||
|
]]>
|
||||||
|
</argument>
|
||||||
|
</arguments>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
@ -29,48 +29,49 @@ import java.util.ArrayList;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import com.kingsrook.qqq.backend.core.actions.CountAction;
|
||||||
import java.util.concurrent.CompletionException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import com.kingsrook.qqq.backend.core.actions.DeleteAction;
|
import com.kingsrook.qqq.backend.core.actions.DeleteAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.InsertAction;
|
import com.kingsrook.qqq.backend.core.actions.InsertAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.MetaDataAction;
|
import com.kingsrook.qqq.backend.core.actions.MetaDataAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.ProcessMetaDataAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.QueryAction;
|
import com.kingsrook.qqq.backend.core.actions.QueryAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.RunProcessAction;
|
|
||||||
import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction;
|
import com.kingsrook.qqq.backend.core.actions.TableMetaDataAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.UpdateAction;
|
import com.kingsrook.qqq.backend.core.actions.UpdateAction;
|
||||||
import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
|
import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
|
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractQRequest;
|
import com.kingsrook.qqq.backend.core.model.actions.AbstractQRequest;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.count.CountRequest;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.count.CountResult;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest;
|
import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteRequest;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteResult;
|
import com.kingsrook.qqq.backend.core.model.actions.delete.DeleteResult;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest;
|
import com.kingsrook.qqq.backend.core.model.actions.insert.InsertRequest;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.insert.InsertResult;
|
import com.kingsrook.qqq.backend.core.model.actions.insert.InsertResult;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataRequest;
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataRequest;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataResult;
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataResult;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataRequest;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.process.ProcessMetaDataResult;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataRequest;
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataRequest;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult;
|
import com.kingsrook.qqq.backend.core.model.actions.metadata.table.TableMetaDataResult;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessRequest;
|
import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessResult;
|
import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
|
import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.query.QueryRequest;
|
import com.kingsrook.qqq.backend.core.model.actions.query.QueryRequest;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.query.QueryResult;
|
import com.kingsrook.qqq.backend.core.model.actions.query.QueryResult;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.update.UpdateRequest;
|
import com.kingsrook.qqq.backend.core.model.actions.update.UpdateRequest;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.update.UpdateResult;
|
import com.kingsrook.qqq.backend.core.model.actions.update.UpdateResult;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
import com.kingsrook.qqq.backend.core.modules.QAuthenticationModuleDispatcher;
|
import com.kingsrook.qqq.backend.core.modules.QAuthenticationModuleDispatcher;
|
||||||
import com.kingsrook.qqq.backend.core.modules.interfaces.QAuthenticationModuleInterface;
|
import com.kingsrook.qqq.backend.core.modules.interfaces.QAuthenticationModuleInterface;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
|
||||||
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
import io.javalin.apibuilder.EndpointGroup;
|
import io.javalin.apibuilder.EndpointGroup;
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
||||||
@ -97,9 +98,11 @@ public class QJavalinImplementation
|
|||||||
|
|
||||||
private static final int SESSION_COOKIE_AGE = 60 * 60 * 24;
|
private static final int SESSION_COOKIE_AGE = 60 * 60 * 24;
|
||||||
|
|
||||||
private static QInstance qInstance;
|
static QInstance qInstance;
|
||||||
|
|
||||||
private static int PORT = 8001;
|
private static int DEFAULT_PORT = 8001;
|
||||||
|
|
||||||
|
private static Javalin service;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -112,7 +115,7 @@ public class QJavalinImplementation
|
|||||||
// todo - parse args to look up metaData and prime instance
|
// todo - parse args to look up metaData and prime instance
|
||||||
// qInstance.addBackend(QMetaDataProvider.getQBackend());
|
// qInstance.addBackend(QMetaDataProvider.getQBackend());
|
||||||
|
|
||||||
new QJavalinImplementation(qInstance).startJavalinServer(PORT);
|
new QJavalinImplementation(qInstance).startJavalinServer(DEFAULT_PORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -145,13 +148,33 @@ public class QJavalinImplementation
|
|||||||
void startJavalinServer(int port)
|
void startJavalinServer(int port)
|
||||||
{
|
{
|
||||||
// todo port from arg
|
// todo port from arg
|
||||||
// todo base path from arg?
|
// todo base path from arg? - and then potentially multiple instances too (chosen based on the root path??)
|
||||||
Javalin service = Javalin.create().start(port);
|
service = Javalin.create().start(port);
|
||||||
service.routes(getRoutes());
|
service.routes(getRoutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
void stopJavalinServer()
|
||||||
|
{
|
||||||
|
service.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static void setDefaultPort(int port)
|
||||||
|
{
|
||||||
|
QJavalinImplementation.DEFAULT_PORT = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -162,10 +185,13 @@ public class QJavalinImplementation
|
|||||||
path("/metaData", () ->
|
path("/metaData", () ->
|
||||||
{
|
{
|
||||||
get("/", QJavalinImplementation::metaData);
|
get("/", QJavalinImplementation::metaData);
|
||||||
path("/:table", () ->
|
path("/table/:table", () ->
|
||||||
{
|
{
|
||||||
get("", QJavalinImplementation::tableMetaData);
|
get("", QJavalinImplementation::tableMetaData);
|
||||||
// todo - process meta data - just under tables? or top-level too? maybe move tables to be under /tables/?
|
});
|
||||||
|
path("/process/:processName", () ->
|
||||||
|
{
|
||||||
|
get("", QJavalinImplementation::processMetaData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
path("/data", () ->
|
path("/data", () ->
|
||||||
@ -174,6 +200,8 @@ public class QJavalinImplementation
|
|||||||
{
|
{
|
||||||
get("/", QJavalinImplementation::dataQuery);
|
get("/", QJavalinImplementation::dataQuery);
|
||||||
post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single.
|
post("/", QJavalinImplementation::dataInsert); // todo - internal to that method, if input is a list, do a bulk - else, single.
|
||||||
|
get("/count", QJavalinImplementation::dataCount);
|
||||||
|
|
||||||
// todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records.
|
// todo - add put and/or patch at this level (without a primaryKey) to do a bulk update based on primaryKeys in the records.
|
||||||
path("/:primaryKey", () ->
|
path("/:primaryKey", () ->
|
||||||
{
|
{
|
||||||
@ -184,14 +212,7 @@ public class QJavalinImplementation
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
path("/processes", () ->
|
path("", QJavalinProcessHandler.getRoutes());
|
||||||
{
|
|
||||||
path("/:process", () ->
|
|
||||||
{
|
|
||||||
get("/init", QJavalinImplementation::processInit);
|
|
||||||
get("/step", QJavalinImplementation::processStep);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,7 +221,7 @@ public class QJavalinImplementation
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static void setupSession(Context context, AbstractQRequest request) throws QModuleDispatchException
|
static void setupSession(Context context, AbstractQRequest request) throws QModuleDispatchException
|
||||||
{
|
{
|
||||||
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
|
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
|
||||||
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData());
|
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData());
|
||||||
@ -334,7 +355,85 @@ public class QJavalinImplementation
|
|||||||
********************************************************************************/
|
********************************************************************************/
|
||||||
private static void dataGet(Context context)
|
private static void dataGet(Context context)
|
||||||
{
|
{
|
||||||
context.result("{\"todo\":\"not-done\",\"getResult\":{}}");
|
try
|
||||||
|
{
|
||||||
|
String tableName = context.pathParam("table");
|
||||||
|
QTableMetaData table = qInstance.getTable(tableName);
|
||||||
|
String primaryKey = context.pathParam("primaryKey");
|
||||||
|
QueryRequest queryRequest = new QueryRequest(qInstance);
|
||||||
|
|
||||||
|
setupSession(context, queryRequest);
|
||||||
|
queryRequest.setTableName(tableName);
|
||||||
|
|
||||||
|
// todo - validate that the primary key is of the proper type (e.g,. not a string for an id field)
|
||||||
|
// and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// setup a filter for the primaryKey = the path-pram //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
queryRequest.setFilter(new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria()
|
||||||
|
.withFieldName(table.getPrimaryKeyField())
|
||||||
|
.withOperator(QCriteriaOperator.EQUALS)
|
||||||
|
.withValues(List.of(primaryKey))));
|
||||||
|
|
||||||
|
QueryAction queryAction = new QueryAction();
|
||||||
|
QueryResult queryResult = queryAction.execute(queryRequest);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// throw a not found error if the record isn't found //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
if(queryResult.getRecords().isEmpty())
|
||||||
|
{
|
||||||
|
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
|
||||||
|
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
context.result(JsonUtils.toJson(queryResult.getRecords().get(0)));
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
handleException(context, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
*
|
||||||
|
* Filter parameter is a serialized QQueryFilter object, that is to say:
|
||||||
|
* <pre>
|
||||||
|
* filter=
|
||||||
|
* {"criteria":[
|
||||||
|
* {"fieldName":"id","operator":"EQUALS","values":[1]},
|
||||||
|
* {"fieldName":"name","operator":"IN","values":["Darin","James"]}
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*******************************************************************************/
|
||||||
|
static void dataCount(Context context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CountRequest countRequest = new CountRequest(qInstance);
|
||||||
|
setupSession(context, countRequest);
|
||||||
|
countRequest.setTableName(context.pathParam("table"));
|
||||||
|
|
||||||
|
String filter = stringQueryParam(context, "filter");
|
||||||
|
if(filter != null)
|
||||||
|
{
|
||||||
|
countRequest.setFilter(JsonUtils.toObject(filter, QQueryFilter.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
CountAction countAction = new CountAction();
|
||||||
|
CountResult countResult = countAction.execute(countRequest);
|
||||||
|
|
||||||
|
context.result(JsonUtils.toJson(countResult));
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
handleException(context, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -430,15 +529,46 @@ public class QJavalinImplementation
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static void handleException(Context context, Exception e)
|
private static void processMetaData(Context context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessMetaDataRequest processMetaDataRequest = new ProcessMetaDataRequest(qInstance);
|
||||||
|
setupSession(context, processMetaDataRequest);
|
||||||
|
processMetaDataRequest.setProcessName(context.pathParam("processName"));
|
||||||
|
ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction();
|
||||||
|
ProcessMetaDataResult processMetaDataResult = processMetaDataAction.execute(processMetaDataRequest);
|
||||||
|
|
||||||
|
context.result(JsonUtils.toJson(processMetaDataResult));
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
handleException(context, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static void handleException(Context context, Exception e)
|
||||||
{
|
{
|
||||||
QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
|
QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(e, QUserFacingException.class);
|
||||||
if(userFacingException != null)
|
if(userFacingException != null)
|
||||||
|
{
|
||||||
|
if(userFacingException instanceof QNotFoundException)
|
||||||
|
{
|
||||||
|
context.status(HttpStatus.NOT_FOUND_404)
|
||||||
|
.result("{\"error\":\"" + userFacingException.getMessage() + "\"}");
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
LOG.info("User-facing exception", e);
|
LOG.info("User-facing exception", e);
|
||||||
context.status(HttpStatus.INTERNAL_SERVER_ERROR_500)
|
context.status(HttpStatus.INTERNAL_SERVER_ERROR_500)
|
||||||
.result("{\"error\":\"" + userFacingException.getMessage() + "\"}");
|
.result("{\"error\":\"" + userFacingException.getMessage() + "\"}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LOG.warn("Exception in javalin request", e);
|
LOG.warn("Exception in javalin request", e);
|
||||||
@ -451,15 +581,15 @@ public class QJavalinImplementation
|
|||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Returns Integer if context has a valid int query parameter by the given name,
|
** Returns Integer if context has a valid int query parameter by the given name,
|
||||||
* Returns null if no param (or empty value).
|
** Returns null if no param (or empty value).
|
||||||
* Throws NumberFormatException for malformed numbers.
|
** Throws QValueException for malformed numbers.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static Integer integerQueryParam(Context context, String name) throws NumberFormatException
|
public static Integer integerQueryParam(Context context, String name) throws QValueException
|
||||||
{
|
{
|
||||||
String value = context.queryParam(name);
|
String value = context.queryParam(name);
|
||||||
if(StringUtils.hasContent(value))
|
if(StringUtils.hasContent(value))
|
||||||
{
|
{
|
||||||
return (Integer.parseInt(value));
|
return (ValueUtils.getValueAsInteger(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (null);
|
return (null);
|
||||||
@ -471,7 +601,7 @@ public class QJavalinImplementation
|
|||||||
** Returns String if context has a valid query parameter by the given name,
|
** Returns String if context has a valid query parameter by the given name,
|
||||||
* Returns null if no param (or empty value).
|
* Returns null if no param (or empty value).
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static String stringQueryParam(Context context, String name) throws NumberFormatException
|
private static String stringQueryParam(Context context, String name)
|
||||||
{
|
{
|
||||||
String value = context.queryParam(name);
|
String value = context.queryParam(name);
|
||||||
if(StringUtils.hasContent(value))
|
if(StringUtils.hasContent(value))
|
||||||
@ -482,87 +612,4 @@ public class QJavalinImplementation
|
|||||||
return (null);
|
return (null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Init a process (named in path param :process)
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private static void processInit(Context context) throws QException
|
|
||||||
{
|
|
||||||
RunProcessRequest runProcessRequest = new RunProcessRequest(qInstance);
|
|
||||||
setupSession(context, runProcessRequest);
|
|
||||||
runProcessRequest.setProcessName(context.pathParam("process"));
|
|
||||||
runProcessRequest.setCallback(new QJavalinProcessCallback());
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// take values from query-string params, and put them into the run process request //
|
|
||||||
// todo - better from POST body, or with a "field-" type of prefix?? //
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
for(Map.Entry<String, List<String>> queryParam : context.queryParamMap().entrySet())
|
|
||||||
{
|
|
||||||
String fieldName = queryParam.getKey();
|
|
||||||
List<String> values = queryParam.getValue();
|
|
||||||
if(CollectionUtils.nullSafeHasContents(values))
|
|
||||||
{
|
|
||||||
runProcessRequest.addValue(fieldName, values.get(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
// run the process //
|
|
||||||
// todo - some "job id" to return to caller? //
|
|
||||||
////////////////////////////////////////////////
|
|
||||||
CompletableFuture<RunProcessResult> future = CompletableFuture.supplyAsync(() ->
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
LOG.info("Running process [" + runProcessRequest.getProcessName() + "]");
|
|
||||||
RunProcessResult runProcessResult = new RunProcessAction().execute(runProcessRequest);
|
|
||||||
LOG.info("Process result error? " + runProcessResult.getError());
|
|
||||||
for(QFieldMetaData outputField : qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields())
|
|
||||||
{
|
|
||||||
LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName()));
|
|
||||||
}
|
|
||||||
return (runProcessResult);
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
LOG.error("Error running future for process", e);
|
|
||||||
throw (new CompletionException(e));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, Object> resultForCaller = new HashMap<>();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
RunProcessResult runProcessResult = future.get(3, TimeUnit.SECONDS);
|
|
||||||
resultForCaller.put("error", runProcessResult.getError());
|
|
||||||
resultForCaller.put("values", runProcessResult.getValues());
|
|
||||||
}
|
|
||||||
catch(TimeoutException te)
|
|
||||||
{
|
|
||||||
resultForCaller.put("jobId", "Job is running asynchronously... job id available in a later version.");
|
|
||||||
}
|
|
||||||
context.result(JsonUtils.toJson(resultForCaller));
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
handleException(context, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Run a step in a process (named in path param :process)
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
private static void processStep(Context context)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,448 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.javalin;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.RunProcessAction;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException;
|
||||||
|
import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
|
||||||
|
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.QCriteriaOperator;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
|
import io.javalin.apibuilder.EndpointGroup;
|
||||||
|
import io.javalin.http.Context;
|
||||||
|
import org.apache.commons.lang.NotImplementedException;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import static io.javalin.apibuilder.ApiBuilder.get;
|
||||||
|
import static io.javalin.apibuilder.ApiBuilder.path;
|
||||||
|
import static io.javalin.apibuilder.ApiBuilder.post;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** methods for handling qqq processes in javalin.
|
||||||
|
*******************************************************************************/
|
||||||
|
public class QJavalinProcessHandler
|
||||||
|
{
|
||||||
|
private static final Logger LOG = LogManager.getLogger(QJavalinProcessHandler.class);
|
||||||
|
|
||||||
|
private static int ASYNC_STEP_TIMEOUT_MILLIS = 3_000;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Define the routes
|
||||||
|
*******************************************************************************/
|
||||||
|
public static EndpointGroup getRoutes()
|
||||||
|
{
|
||||||
|
return (() ->
|
||||||
|
{
|
||||||
|
path("/processes", () ->
|
||||||
|
{
|
||||||
|
path("/:processName", () ->
|
||||||
|
{
|
||||||
|
get("/init", QJavalinProcessHandler::processInit);
|
||||||
|
post("/init", QJavalinProcessHandler::processInit);
|
||||||
|
|
||||||
|
path("/:processUUID", () ->
|
||||||
|
{
|
||||||
|
post("/step/:step", QJavalinProcessHandler::processStep);
|
||||||
|
get("/status/:jobUUID", QJavalinProcessHandler::processStatus);
|
||||||
|
get("/records", QJavalinProcessHandler::processRecords);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Init a process (named in path param :process)
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static void processInit(Context context) throws QException
|
||||||
|
{
|
||||||
|
doProcessInitOrStep(context, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void doProcessInitOrStep(Context context, String processUUID, String startAfterStep)
|
||||||
|
{
|
||||||
|
Map<String, Object> resultForCaller = new HashMap<>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(processUUID == null)
|
||||||
|
{
|
||||||
|
processUUID = UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
|
resultForCaller.put("processUUID", processUUID);
|
||||||
|
|
||||||
|
String processName = context.pathParam("processName");
|
||||||
|
LOG.info(startAfterStep == null ? "Initiating process [" + processName + "] [" + processUUID + "]"
|
||||||
|
: "Resuming process [" + processName + "] [" + processUUID + "] after step [" + startAfterStep + "]");
|
||||||
|
|
||||||
|
RunProcessRequest runProcessRequest = new RunProcessRequest(QJavalinImplementation.qInstance);
|
||||||
|
QJavalinImplementation.setupSession(context, runProcessRequest);
|
||||||
|
runProcessRequest.setProcessName(processName);
|
||||||
|
runProcessRequest.setFrontendStepBehavior(RunProcessRequest.FrontendStepBehavior.BREAK);
|
||||||
|
runProcessRequest.setProcessUUID(processUUID);
|
||||||
|
runProcessRequest.setStartAfterStep(startAfterStep);
|
||||||
|
populateRunProcessRequestWithValuesFromContext(context, runProcessRequest);
|
||||||
|
|
||||||
|
////////////////////////////////////////
|
||||||
|
// run the process as an async action //
|
||||||
|
////////////////////////////////////////
|
||||||
|
Integer timeout = getTimeoutMillis(context);
|
||||||
|
RunProcessResult runProcessResult = new AsyncJobManager().startJob(timeout, TimeUnit.MILLISECONDS, (callback) ->
|
||||||
|
{
|
||||||
|
runProcessRequest.setAsyncJobCallback(callback);
|
||||||
|
return (new RunProcessAction().execute(runProcessRequest));
|
||||||
|
});
|
||||||
|
|
||||||
|
LOG.info("Process result error? " + runProcessResult.getException());
|
||||||
|
for(QFieldMetaData outputField : QJavalinImplementation.qInstance.getProcess(runProcessRequest.getProcessName()).getOutputFields())
|
||||||
|
{
|
||||||
|
LOG.info("Process result output value: " + outputField.getName() + ": " + runProcessResult.getValues().get(outputField.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
serializeRunProcessResultForCaller(resultForCaller, runProcessResult);
|
||||||
|
}
|
||||||
|
catch(JobGoingAsyncException jgae)
|
||||||
|
{
|
||||||
|
resultForCaller.put("jobUUID", jgae.getJobUUID());
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
// our other actions in here would do: handleException(context, e); //
|
||||||
|
// which would return a 500 to the client. //
|
||||||
|
// but - other process-step actions, they always return a 200, just with an //
|
||||||
|
// optional error message - so - keep all of the processes consistent. //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////
|
||||||
|
serializeRunProcessExceptionForCaller(resultForCaller, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.result(JsonUtils.toJson(resultForCaller));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Whether a step finished synchronously or asynchronously, return its data
|
||||||
|
** to the caller the same way.
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void serializeRunProcessResultForCaller(Map<String, Object> resultForCaller, RunProcessResult runProcessResult)
|
||||||
|
{
|
||||||
|
if(runProcessResult.getException().isPresent())
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// per code coverage, this path may never actually get hit... //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
serializeRunProcessExceptionForCaller(resultForCaller, runProcessResult.getException().get());
|
||||||
|
}
|
||||||
|
resultForCaller.put("values", runProcessResult.getValues());
|
||||||
|
runProcessResult.getProcessState().getNextStepName().ifPresent(nextStep -> resultForCaller.put("nextStep", nextStep));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void serializeRunProcessExceptionForCaller(Map<String, Object> resultForCaller, Exception exception)
|
||||||
|
{
|
||||||
|
QUserFacingException userFacingException = ExceptionUtils.findClassInRootChain(exception, QUserFacingException.class);
|
||||||
|
|
||||||
|
if(userFacingException != null)
|
||||||
|
{
|
||||||
|
LOG.info("User-facing exception in process", userFacingException);
|
||||||
|
resultForCaller.put("error", userFacingException.getMessage()); // todo - put this somewhere else (make error an object w/ user-facing and/or other error?)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Throwable rootException = ExceptionUtils.getRootException(exception);
|
||||||
|
LOG.warn("Uncaught Exception in process", exception);
|
||||||
|
resultForCaller.put("error", "Original error message: " + rootException.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** take values from query-string params, and put them into the run process request
|
||||||
|
** todo - better from POST body, or with a "field-" type of prefix??
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void populateRunProcessRequestWithValuesFromContext(Context context, RunProcessRequest runProcessRequest) throws IOException
|
||||||
|
{
|
||||||
|
for(Map.Entry<String, List<String>> queryParam : context.queryParamMap().entrySet())
|
||||||
|
{
|
||||||
|
String fieldName = queryParam.getKey();
|
||||||
|
List<String> values = queryParam.getValue();
|
||||||
|
if(CollectionUtils.nullSafeHasContents(values))
|
||||||
|
{
|
||||||
|
runProcessRequest.addValue(fieldName, values.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQueryFilter initialRecordsFilter = buildProcessInitRecordsFilter(context, runProcessRequest);
|
||||||
|
if(initialRecordsFilter != null)
|
||||||
|
{
|
||||||
|
runProcessRequest.setCallback(new QProcessCallback()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public QQueryFilter getQueryFilter()
|
||||||
|
{
|
||||||
|
return (initialRecordsFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields)
|
||||||
|
{
|
||||||
|
return (Collections.emptyMap());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static QQueryFilter buildProcessInitRecordsFilter(Context context, RunProcessRequest runProcessRequest) throws IOException
|
||||||
|
{
|
||||||
|
QInstance instance = runProcessRequest.getInstance();
|
||||||
|
QProcessMetaData process = instance.getProcess(runProcessRequest.getProcessName());
|
||||||
|
QTableMetaData table = instance.getTable(process.getTableName());
|
||||||
|
|
||||||
|
if(table == null)
|
||||||
|
{
|
||||||
|
LOG.info("No table found in process - so not building an init records filter.");
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
String primaryKeyField = table.getPrimaryKeyField();
|
||||||
|
|
||||||
|
String recordsParam = context.queryParam("recordsParam");
|
||||||
|
if(StringUtils.hasContent(recordsParam))
|
||||||
|
{
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
String paramValue = context.queryParam(recordsParam);
|
||||||
|
if(!StringUtils.hasContent(paramValue))
|
||||||
|
{
|
||||||
|
throw (new IllegalArgumentException("Missing value in query parameter: " + recordsParam + " (which was specified as the recordsParam)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(recordsParam)
|
||||||
|
{
|
||||||
|
case "recordIds":
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
Serializable[] idStrings = paramValue.split(",");
|
||||||
|
return (new QQueryFilter().withCriteria(new QFilterCriteria()
|
||||||
|
.withFieldName(primaryKeyField)
|
||||||
|
.withOperator(QCriteriaOperator.IN)
|
||||||
|
.withValues(Arrays.stream(idStrings).toList())));
|
||||||
|
case "filterJSON":
|
||||||
|
return (JsonUtils.toObject(paramValue, QQueryFilter.class));
|
||||||
|
case "filterId":
|
||||||
|
// return (JsonUtils.toObject(context.queryParam(recordsParam), QQueryFilter.class));
|
||||||
|
throw (new NotImplementedException("Saved filters are not yet implemented."));
|
||||||
|
default:
|
||||||
|
throw (new IllegalArgumentException("Unrecognized value [" + recordsParam + "] for query parameter: recordsParam"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Run a step in a process (named in path param :processName)
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static void processStep(Context context) throws QModuleDispatchException
|
||||||
|
{
|
||||||
|
String processUUID = context.pathParam("processUUID");
|
||||||
|
String lastStep = context.pathParam("step");
|
||||||
|
doProcessInitOrStep(context, processUUID, lastStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Get status for a currently running process (step)
|
||||||
|
*******************************************************************************/
|
||||||
|
public static void processStatus(Context context)
|
||||||
|
{
|
||||||
|
Map<String, Object> resultForCaller = new HashMap<>();
|
||||||
|
|
||||||
|
String processUUID = context.pathParam("processUUID");
|
||||||
|
String jobUUID = context.pathParam("jobUUID");
|
||||||
|
|
||||||
|
LOG.info("Request for status of process " + processUUID + ", job " + jobUUID);
|
||||||
|
Optional<AsyncJobStatus> optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
|
||||||
|
if(optionalJobStatus.isEmpty())
|
||||||
|
{
|
||||||
|
serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find status of process step job"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AsyncJobStatus jobStatus = optionalJobStatus.get();
|
||||||
|
|
||||||
|
resultForCaller.put("jobStatus", jobStatus);
|
||||||
|
LOG.info("Job status is " + jobStatus.getState() + " for " + jobUUID);
|
||||||
|
|
||||||
|
if(jobStatus.getState().equals(AsyncJobState.COMPLETE))
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the job is complete, get the process result from state provider, and return it //
|
||||||
|
// this output should look like it did if the job finished synchronously!! //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Optional<ProcessState> processState = RunProcessAction.getState(processUUID);
|
||||||
|
if(processState.isPresent())
|
||||||
|
{
|
||||||
|
RunProcessResult runProcessResult = new RunProcessResult(processState.get());
|
||||||
|
serializeRunProcessResultForCaller(resultForCaller, runProcessResult);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find results for process " + processUUID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(jobStatus.getState().equals(AsyncJobState.ERROR))
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(jobStatus.getCaughtException() != null)
|
||||||
|
{
|
||||||
|
serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.result(JsonUtils.toJson(resultForCaller));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static void processRecords(Context context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
String processUUID = context.pathParam("processUUID");
|
||||||
|
Integer skip = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "skip"), 0);
|
||||||
|
Integer limit = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "limit"), 20);
|
||||||
|
|
||||||
|
// todo - potential optimization - if a future state provider could take advantage of it,
|
||||||
|
// we might pass the skip & limit in to a method that fetch just those 'n' rows from state, rather than the whole thing?
|
||||||
|
Optional<ProcessState> optionalProcessState = RunProcessAction.getState(processUUID);
|
||||||
|
if(optionalProcessState.isEmpty())
|
||||||
|
{
|
||||||
|
throw (new Exception("Could not find process results."));
|
||||||
|
}
|
||||||
|
ProcessState processState = optionalProcessState.get();
|
||||||
|
|
||||||
|
List<QRecord> records = processState.getRecords();
|
||||||
|
if(CollectionUtils.nullSafeIsEmpty(records))
|
||||||
|
{
|
||||||
|
throw (new Exception("No records were found for the process."));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> resultForCaller = new HashMap<>();
|
||||||
|
List<QRecord> recordPage = CollectionUtils.safelyGetPage(records, skip, limit);
|
||||||
|
resultForCaller.put("records", recordPage);
|
||||||
|
context.result(JsonUtils.toJson(resultForCaller));
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
QJavalinImplementation.handleException(context, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static void setAsyncStepTimeoutMillis(int asyncStepTimeoutMillis)
|
||||||
|
{
|
||||||
|
ASYNC_STEP_TIMEOUT_MILLIS = asyncStepTimeoutMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private static Integer getTimeoutMillis(Context context)
|
||||||
|
{
|
||||||
|
Integer timeout = QJavalinImplementation.integerQueryParam(context, "_qStepTimeoutMillis");
|
||||||
|
if(timeout == null)
|
||||||
|
{
|
||||||
|
timeout = ASYNC_STEP_TIMEOUT_MILLIS;
|
||||||
|
}
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -33,10 +33,9 @@ import kong.unirest.Unirest;
|
|||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@ -49,37 +48,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||||||
** and actually makes http requests into it.
|
** and actually makes http requests into it.
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
class QJavalinImplementationTest
|
class QJavalinImplementationTest extends QJavalinTestBase
|
||||||
{
|
{
|
||||||
private static final int PORT = 6262;
|
|
||||||
private static final String BASE_URL = "http://localhost:" + PORT;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Before the class (all) runs, start a javalin server.
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@BeforeAll
|
|
||||||
public static void beforeAll() throws Exception
|
|
||||||
{
|
|
||||||
QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance());
|
|
||||||
qJavalinImplementation.startJavalinServer(PORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** Fully rebuild the test-database before each test runs, for completely known state.
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@BeforeEach
|
|
||||||
public void beforeEach() throws Exception
|
|
||||||
{
|
|
||||||
TestUtils.primeTestDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** test the top-level meta-data endpoint
|
** test the top-level meta-data endpoint
|
||||||
@ -95,11 +68,15 @@ class QJavalinImplementationTest
|
|||||||
assertTrue(jsonObject.has("tables"));
|
assertTrue(jsonObject.has("tables"));
|
||||||
JSONObject tables = jsonObject.getJSONObject("tables");
|
JSONObject tables = jsonObject.getJSONObject("tables");
|
||||||
assertEquals(1, tables.length());
|
assertEquals(1, tables.length());
|
||||||
JSONObject table0 = tables.getJSONObject("person");
|
JSONObject personTable = tables.getJSONObject("person");
|
||||||
assertTrue(table0.has("name"));
|
assertTrue(personTable.has("name"));
|
||||||
assertEquals("person", table0.getString("name"));
|
assertEquals("person", personTable.getString("name"));
|
||||||
assertTrue(table0.has("label"));
|
assertTrue(personTable.has("label"));
|
||||||
assertEquals("Person", table0.getString("label"));
|
assertEquals("Person", personTable.getString("label"));
|
||||||
|
assertFalse(personTable.getBoolean("isHidden"));
|
||||||
|
|
||||||
|
JSONObject processes = jsonObject.getJSONObject("processes");
|
||||||
|
assertTrue(processes.getJSONObject("simpleSleep").getBoolean("isHidden"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -111,13 +88,12 @@ class QJavalinImplementationTest
|
|||||||
@Test
|
@Test
|
||||||
public void test_tableMetaData()
|
public void test_tableMetaData()
|
||||||
{
|
{
|
||||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/person").asString();
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/table/person").asString();
|
||||||
|
|
||||||
assertEquals(200, response.getStatus());
|
assertEquals(200, response.getStatus());
|
||||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||||
JSONObject table = jsonObject.getJSONObject("table");
|
JSONObject table = jsonObject.getJSONObject("table");
|
||||||
assertEquals(4, table.keySet().size(), "Number of mid-level keys");
|
|
||||||
assertEquals("person", table.getString("name"));
|
assertEquals("person", table.getString("name"));
|
||||||
assertEquals("Person", table.getString("label"));
|
assertEquals("Person", table.getString("label"));
|
||||||
assertEquals("id", table.getString("primaryKeyField"));
|
assertEquals("id", table.getString("primaryKeyField"));
|
||||||
@ -136,9 +112,9 @@ class QJavalinImplementationTest
|
|||||||
@Test
|
@Test
|
||||||
public void test_tableMetaData_notFound()
|
public void test_tableMetaData_notFound()
|
||||||
{
|
{
|
||||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/notAnActualTable").asString();
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/table/notAnActualTable").asString();
|
||||||
|
|
||||||
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus()); // todo 404?
|
assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
|
||||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||||
String error = jsonObject.getString("error");
|
String error = jsonObject.getString("error");
|
||||||
@ -147,6 +123,121 @@ class QJavalinImplementationTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test the process-level meta-data endpoint
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processMetaData()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/process/greetInteractive").asString();
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||||
|
JSONObject process = jsonObject.getJSONObject("process");
|
||||||
|
assertEquals("greetInteractive", process.getString("name"));
|
||||||
|
assertEquals("Greet Interactive", process.getString("label"));
|
||||||
|
assertEquals("person", process.getString("tableName"));
|
||||||
|
|
||||||
|
JSONArray frontendSteps = process.getJSONArray("frontendSteps");
|
||||||
|
JSONObject setupStep = frontendSteps.getJSONObject(0);
|
||||||
|
assertEquals("Setup", setupStep.getString("label"));
|
||||||
|
JSONArray setupFields = setupStep.getJSONArray("formFields");
|
||||||
|
assertEquals(2, setupFields.length());
|
||||||
|
assertTrue(setupFields.toList().stream().anyMatch(field -> "greetingPrefix".equals(((Map<?, ?>) field).get("name"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test the process-level meta-data endpoint for a non-real name
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processMetaData_notFound()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData/process/notAnActualProcess").asString();
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||||
|
String error = jsonObject.getString("error");
|
||||||
|
assertTrue(error.contains("not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test a table get (single record)
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_dataGet()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/1").asString();
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertTrue(jsonObject.has("values"));
|
||||||
|
assertEquals("person", jsonObject.getString("tableName"));
|
||||||
|
JSONObject values = jsonObject.getJSONObject("values");
|
||||||
|
assertTrue(values.has("firstName"));
|
||||||
|
assertTrue(values.has("id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test a table get (single record) for an id that isn't found
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_dataGetNotFound()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/98765").asString();
|
||||||
|
assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||||
|
String error = jsonObject.getString("error");
|
||||||
|
assertEquals("Could not find Person with Id of 98765", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test a table get (single record) for an id that isn't the expected type
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_dataGetWrongIdType()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/not-an-integer").asString();
|
||||||
|
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR_500, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertEquals(1, jsonObject.keySet().size(), "Number of top-level keys");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test a table count
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_dataCount()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/data/person/count").asString();
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertTrue(jsonObject.has("count"));
|
||||||
|
int count = jsonObject.getInt("count");
|
||||||
|
assertEquals(5, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** test a table query
|
** test a table query
|
||||||
**
|
**
|
||||||
@ -226,6 +317,7 @@ class QJavalinImplementationTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** test an update
|
** test an update
|
||||||
**
|
**
|
||||||
@ -265,10 +357,7 @@ class QJavalinImplementationTest
|
|||||||
@Test
|
@Test
|
||||||
public void test_dataDelete() throws Exception
|
public void test_dataDelete() throws Exception
|
||||||
{
|
{
|
||||||
HttpResponse<String> response = Unirest.delete(BASE_URL + "/data/person/3")
|
HttpResponse<String> response = Unirest.delete(BASE_URL + "/data/person/3").asString();
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.asString();
|
|
||||||
|
|
||||||
assertEquals(200, response.getStatus());
|
assertEquals(200, response.getStatus());
|
||||||
|
|
||||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
@ -286,42 +375,4 @@ class QJavalinImplementationTest
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** test running a process
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Test
|
|
||||||
public void test_processGreetInit() throws Exception
|
|
||||||
{
|
|
||||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init")
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.asString();
|
|
||||||
|
|
||||||
assertEquals(200, response.getStatus());
|
|
||||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
|
||||||
assertNotNull(jsonObject);
|
|
||||||
assertEquals("null X null", jsonObject.getJSONObject("values").getString("outputMessage"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
|
||||||
** test running a process with field values on the query string
|
|
||||||
**
|
|
||||||
*******************************************************************************/
|
|
||||||
@Test
|
|
||||||
public void test_processGreetInitWithQueryValues() throws Exception
|
|
||||||
{
|
|
||||||
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init?greetingPrefix=Hey&greetingSuffix=Jude")
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.asString();
|
|
||||||
|
|
||||||
assertEquals(200, response.getStatus());
|
|
||||||
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
|
||||||
assertNotNull(jsonObject);
|
|
||||||
assertEquals("Hey X Jude", jsonObject.getJSONObject("values").getString("outputMessage"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,479 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qqq.backend.javalin;
|
||||||
|
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.List;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.query.QCriteriaOperator;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||||
|
import kong.unirest.HttpResponse;
|
||||||
|
import kong.unirest.Unirest;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Unit test for the javalin process handler methods.
|
||||||
|
*******************************************************************************/
|
||||||
|
class QJavalinProcessHandlerTest extends QJavalinTestBase
|
||||||
|
{
|
||||||
|
private static final int MORE_THAN_TIMEOUT = 500;
|
||||||
|
private static final int LESS_THAN_TIMEOUT = 50;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test running a process
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processGreetInit()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3").asString();
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertNotNull(jsonObject);
|
||||||
|
assertEquals("null X null", jsonObject.getJSONObject("values").getString("outputMessage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test running a process that requires rows, but we didn't tell it how to get them.
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processRequiresRowsButNotSpecified()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init").asString();
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertNotNull(jsonObject);
|
||||||
|
assertTrue(jsonObject.has("error"));
|
||||||
|
assertTrue(jsonObject.getString("error").contains("Missing input records"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test running a process and telling it rows to load via recordIds param
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processRequiresRowsWithRecordIdParam()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3").asString();
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertNotNull(jsonObject);
|
||||||
|
String processUUID = jsonObject.getString("processUUID");
|
||||||
|
|
||||||
|
getProcessRecords(processUUID, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test running a process and telling it rows to load via filter JSON
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processRequiresRowsWithFilterJSON()
|
||||||
|
{
|
||||||
|
QQueryFilter queryFilter = new QQueryFilter()
|
||||||
|
.withCriteria(new QFilterCriteria()
|
||||||
|
.withFieldName("id")
|
||||||
|
.withOperator(QCriteriaOperator.IN)
|
||||||
|
.withValues(List.of(3, 4, 5)));
|
||||||
|
String filterJSON = JsonUtils.toJson(queryFilter);
|
||||||
|
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(filterJSON, Charset.defaultCharset())).asString();
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertNotNull(jsonObject);
|
||||||
|
String processUUID = jsonObject.getString("processUUID");
|
||||||
|
|
||||||
|
getProcessRecords(processUUID, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private JSONObject getProcessRecords(String processUUID, int expectedNoOfRecords)
|
||||||
|
{
|
||||||
|
return (getProcessRecords(processUUID, expectedNoOfRecords, 0, 20));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
private JSONObject getProcessRecords(String processUUID, int expectedNoOfRecords, int skip, int limit)
|
||||||
|
{
|
||||||
|
HttpResponse<String> response;
|
||||||
|
JSONObject jsonObject;
|
||||||
|
response = Unirest.get(BASE_URL + "/processes/greet/" + processUUID + "/records?skip=" + skip + "&limit=" + limit).asString();
|
||||||
|
jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertNotNull(jsonObject);
|
||||||
|
|
||||||
|
if(expectedNoOfRecords == 0)
|
||||||
|
{
|
||||||
|
assertFalse(jsonObject.has("records"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
assertTrue(jsonObject.has("records"));
|
||||||
|
JSONArray records = jsonObject.getJSONArray("records");
|
||||||
|
assertEquals(expectedNoOfRecords, records.length());
|
||||||
|
}
|
||||||
|
return (jsonObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test running a process with field values on the query string
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processGreetInitWithQueryValues()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.post(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3&greetingPrefix=Hey&greetingSuffix=Jude").asString();
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertNotNull(jsonObject);
|
||||||
|
assertEquals("Hey X Jude", jsonObject.getJSONObject("values").getString("outputMessage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test init'ing a process that goes async
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processInitGoingAsync() throws InterruptedException
|
||||||
|
{
|
||||||
|
String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP;
|
||||||
|
HttpResponse<String> response = Unirest.get(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString();
|
||||||
|
|
||||||
|
JSONObject jsonObject = assertProcessStepWentAsyncResponse(response);
|
||||||
|
String processUUID = jsonObject.getString("processUUID");
|
||||||
|
String jobUUID = jsonObject.getString("jobUUID");
|
||||||
|
assertNotNull(processUUID, "Process UUID should not be null.");
|
||||||
|
assertNotNull(jobUUID, "Job UUID should not be null");
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// request job status before sleep is done //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
|
||||||
|
jsonObject = assertProcessStepRunningResponse(response);
|
||||||
|
|
||||||
|
///////////////////////////////////
|
||||||
|
// sleep, to let that job finish //
|
||||||
|
///////////////////////////////////
|
||||||
|
Thread.sleep(MORE_THAN_TIMEOUT);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
// request job status again, get back results instead //
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
|
||||||
|
jsonObject = assertProcessStepCompleteResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test init'ing a process that does NOT goes async
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processInitNotGoingAsync()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_SLEEP + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT)
|
||||||
|
.header("Content-Type", "application/json").asString();
|
||||||
|
assertProcessStepCompleteResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test running a step a process that goes async
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processStepGoingAsync() throws InterruptedException
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// first init the process, to get its UUID //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE;
|
||||||
|
HttpResponse<String> response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT)
|
||||||
|
.header("Content-Type", "application/json").asString();
|
||||||
|
|
||||||
|
JSONObject jsonObject = assertProcessStepCompleteResponse(response);
|
||||||
|
String processUUID = jsonObject.getString("processUUID");
|
||||||
|
String nextStep = jsonObject.getString("nextStep");
|
||||||
|
assertNotNull(processUUID, "Process UUID should not be null.");
|
||||||
|
assertNotNull(nextStep, "There should be a next step");
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// second, run the 'nextStep' (the backend step, that sleeps). run it with a long enough sleep so that it'll go async //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep)
|
||||||
|
.header("Content-Type", "application/json").asString();
|
||||||
|
|
||||||
|
jsonObject = assertProcessStepWentAsyncResponse(response);
|
||||||
|
String jobUUID = jsonObject.getString("jobUUID");
|
||||||
|
|
||||||
|
///////////////////////////////////
|
||||||
|
// sleep, to let that job finish //
|
||||||
|
///////////////////////////////////
|
||||||
|
Thread.sleep(MORE_THAN_TIMEOUT);
|
||||||
|
|
||||||
|
///////////////////////////////
|
||||||
|
// third, request job status //
|
||||||
|
///////////////////////////////
|
||||||
|
response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
|
||||||
|
|
||||||
|
jsonObject = assertProcessStepCompleteResponse(response);
|
||||||
|
String nextStep2 = jsonObject.getString("nextStep");
|
||||||
|
assertNotNull(nextStep2, "There be one more next step");
|
||||||
|
assertNotEquals(nextStep, nextStep2, "The next step should be different this time.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test running a step a process that does NOT goes async
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processStepNotGoingAsync()
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// first init the process, to get its UUID //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SLEEP_INTERACTIVE;
|
||||||
|
HttpResponse<String> response = Unirest.post(processBasePath + "/init?" + TestUtils.SleeperStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT)
|
||||||
|
.header("Content-Type", "application/json").asString();
|
||||||
|
|
||||||
|
JSONObject jsonObject = assertProcessStepCompleteResponse(response);
|
||||||
|
String processUUID = jsonObject.getString("processUUID");
|
||||||
|
String nextStep = jsonObject.getString("nextStep");
|
||||||
|
assertNotNull(nextStep, "There should be a next step");
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// second, run the 'nextStep' (the backend step, that sleeps). run it with a short enough sleep so that it won't go async //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
response = Unirest.post(processBasePath + "/" + processUUID + "/step/" + nextStep)
|
||||||
|
.header("Content-Type", "application/json").asString();
|
||||||
|
|
||||||
|
jsonObject = assertProcessStepCompleteResponse(response);
|
||||||
|
String nextStep2 = jsonObject.getString("nextStep");
|
||||||
|
assertNotNull(nextStep2, "There be one more next step");
|
||||||
|
assertNotEquals(nextStep, nextStep2, "The next step should be different this time.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test init'ing a process that goes async and then throws
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processInitGoingAsyncThenThrowing() throws InterruptedException
|
||||||
|
{
|
||||||
|
String processBasePath = BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW;
|
||||||
|
HttpResponse<String> response = Unirest.get(processBasePath + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + MORE_THAN_TIMEOUT).asString();
|
||||||
|
|
||||||
|
JSONObject jsonObject = assertProcessStepWentAsyncResponse(response);
|
||||||
|
String processUUID = jsonObject.getString("processUUID");
|
||||||
|
String jobUUID = jsonObject.getString("jobUUID");
|
||||||
|
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
// request job status before sleep is done //
|
||||||
|
/////////////////////////////////////////////
|
||||||
|
response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
|
||||||
|
jsonObject = assertProcessStepRunningResponse(response);
|
||||||
|
|
||||||
|
///////////////////////////////////
|
||||||
|
// sleep, to let that job finish //
|
||||||
|
///////////////////////////////////
|
||||||
|
Thread.sleep(MORE_THAN_TIMEOUT);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
// request job status again, get back error status instead //
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
response = Unirest.get(processBasePath + "/" + processUUID + "/status/" + jobUUID).asString();
|
||||||
|
jsonObject = assertProcessStepErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test init'ing a process that does NOT goes async, but throws.
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processInitNotGoingAsyncButThrowing()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.post(BASE_URL + "/processes/" + TestUtils.PROCESS_NAME_SIMPLE_THROW + "/init?" + TestUtils.ThrowerStep.FIELD_SLEEP_MILLIS + "=" + LESS_THAN_TIMEOUT)
|
||||||
|
.header("Content-Type", "application/json").asString();
|
||||||
|
assertProcessStepErrorResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** every time a process step (or init) has gone async, expect what the
|
||||||
|
** response should look like
|
||||||
|
*******************************************************************************/
|
||||||
|
private JSONObject assertProcessStepWentAsyncResponse(HttpResponse<String> response)
|
||||||
|
{
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
|
||||||
|
assertTrue(jsonObject.has("processUUID"), "Async-started response should have a processUUID");
|
||||||
|
assertTrue(jsonObject.has("jobUUID"), "Async-started response should have a jobUUID");
|
||||||
|
|
||||||
|
assertFalse(jsonObject.has("values"), "Async-started response should NOT have values");
|
||||||
|
assertFalse(jsonObject.has("error"), "Async-started response should NOT have error");
|
||||||
|
|
||||||
|
return (jsonObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** every time a process step (sync or async) is still running, expect certain things
|
||||||
|
** to be (and not to be) in the json response.
|
||||||
|
*******************************************************************************/
|
||||||
|
private JSONObject assertProcessStepRunningResponse(HttpResponse<String> response)
|
||||||
|
{
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
|
||||||
|
assertTrue(jsonObject.has("jobStatus"), "Step Running response should have a jobStatus");
|
||||||
|
|
||||||
|
assertFalse(jsonObject.has("values"), "Step Running response should NOT have values");
|
||||||
|
assertFalse(jsonObject.has("error"), "Step Running response should NOT have error");
|
||||||
|
|
||||||
|
assertEquals(AsyncJobState.RUNNING.name(), jsonObject.getJSONObject("jobStatus").getString("state"));
|
||||||
|
|
||||||
|
return (jsonObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** every time a process step (sync or async) completes, expect certain things
|
||||||
|
** to be (and not to be) in the json response.
|
||||||
|
*******************************************************************************/
|
||||||
|
private JSONObject assertProcessStepCompleteResponse(HttpResponse<String> response)
|
||||||
|
{
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
|
||||||
|
assertTrue(jsonObject.has("values"), "Step Complete response should have values");
|
||||||
|
|
||||||
|
assertFalse(jsonObject.has("jobUUID"), "Step Complete response should not have a jobUUID");
|
||||||
|
assertFalse(jsonObject.has("error"), "Step Complete response should not have an error");
|
||||||
|
|
||||||
|
return (jsonObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** every time a process step (sync or async) has an error, expect certain things
|
||||||
|
** to be (and not to be) in the json response.
|
||||||
|
*******************************************************************************/
|
||||||
|
private JSONObject assertProcessStepErrorResponse(HttpResponse<String> response)
|
||||||
|
{
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
|
||||||
|
assertTrue(jsonObject.has("error"), "Step Error response should have an error");
|
||||||
|
|
||||||
|
assertFalse(jsonObject.has("jobUUID"), "Step Error response should not have a jobUUID");
|
||||||
|
assertFalse(jsonObject.has("values"), "Step Error response should not have values");
|
||||||
|
|
||||||
|
return (jsonObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test getting records back from a process
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processRecords()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=2,3&greetingPrefix=Hey&greetingSuffix=Jude").asString();
|
||||||
|
assertEquals(200, response.getStatus());
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
assertNotNull(jsonObject);
|
||||||
|
String processUUID = jsonObject.getString("processUUID");
|
||||||
|
|
||||||
|
jsonObject = getProcessRecords(processUUID, 2);
|
||||||
|
JSONArray records = jsonObject.getJSONArray("records");
|
||||||
|
JSONObject record0 = records.getJSONObject(0);
|
||||||
|
JSONObject values = record0.getJSONObject("values");
|
||||||
|
assertTrue(values.has("id"));
|
||||||
|
assertTrue(values.has("firstName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** test getting records back from a process with skip & Limit
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
public void test_processRecordsSkipAndLimit()
|
||||||
|
{
|
||||||
|
HttpResponse<String> response = Unirest.get(BASE_URL + "/processes/greet/init?recordsParam=recordIds&recordIds=1,2,3,4,5").asString();
|
||||||
|
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
|
||||||
|
String processUUID = jsonObject.getString("processUUID");
|
||||||
|
|
||||||
|
getProcessRecords(processUUID, 5);
|
||||||
|
getProcessRecords(processUUID, 1, 4, 5);
|
||||||
|
getProcessRecords(processUUID, 0, 5, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -22,45 +22,57 @@
|
|||||||
package com.kingsrook.qqq.backend.javalin;
|
package com.kingsrook.qqq.backend.javalin;
|
||||||
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import java.util.HashMap;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import java.util.List;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import java.util.Map;
|
|
||||||
import com.kingsrook.qqq.backend.core.callbacks.QProcessCallback;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.query.QQueryFilter;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
** base class for javalin implementation tests.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class QJavalinProcessCallback implements QProcessCallback
|
public class QJavalinTestBase
|
||||||
{
|
{
|
||||||
private static final Logger LOG = LogManager.getLogger(QJavalinProcessCallback.class);
|
private static final int PORT = 6262;
|
||||||
|
protected static final String BASE_URL = "http://localhost:" + PORT;
|
||||||
|
|
||||||
|
private static QJavalinImplementation qJavalinImplementation;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
** Before the class (all) runs, start a javalin server.
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@BeforeAll
|
||||||
public QQueryFilter getQueryFilter()
|
public static void beforeAll()
|
||||||
{
|
{
|
||||||
LOG.warn("Getting a query filter in javalin is NOT yet implemented");
|
qJavalinImplementation = new QJavalinImplementation(TestUtils.defineInstance());
|
||||||
return (new QQueryFilter());
|
QJavalinProcessHandler.setAsyncStepTimeoutMillis(250);
|
||||||
|
qJavalinImplementation.startJavalinServer(PORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
** Before the class (all) runs, start a javalin server.
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Override
|
@AfterAll
|
||||||
public Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields)
|
public static void afterAll()
|
||||||
{
|
{
|
||||||
LOG.warn("Getting field values in javalin is NOT yet implemented");
|
qJavalinImplementation.stopJavalinServer();
|
||||||
return (new HashMap<>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fully rebuild the test-database before each test runs, for completely known state.
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@BeforeEach
|
||||||
|
public void beforeEach() throws Exception
|
||||||
|
{
|
||||||
|
TestUtils.primeTestDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -25,7 +25,12 @@ package com.kingsrook.qqq.backend.javalin;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.kingsrook.qqq.backend.core.interfaces.mock.MockFunctionBody;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
||||||
|
import com.kingsrook.qqq.backend.core.interfaces.BackendStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.interfaces.mock.MockBackendStep;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepRequest;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepResult;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference;
|
import com.kingsrook.qqq.backend.core.model.metadata.QCodeReference;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QCodeType;
|
import com.kingsrook.qqq.backend.core.model.metadata.QCodeType;
|
||||||
@ -34,16 +39,15 @@ import com.kingsrook.qqq.backend.core.model.metadata.QFieldMetaData;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.QFieldType;
|
import com.kingsrook.qqq.backend.core.model.metadata.QFieldType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.QTableMetaData;
|
||||||
|
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.QFunctionInputMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionMetaData;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QOutputView;
|
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
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.model.metadata.processes.QRecordListMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListView;
|
|
||||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
|
||||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
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.jdbc.QueryManager;
|
||||||
|
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import static junit.framework.Assert.assertNotNull;
|
import static junit.framework.Assert.assertNotNull;
|
||||||
|
|
||||||
@ -54,6 +58,18 @@ import static junit.framework.Assert.assertNotNull;
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public class TestUtils
|
public class TestUtils
|
||||||
{
|
{
|
||||||
|
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
|
||||||
|
public static final String PROCESS_NAME_SIMPLE_SLEEP = "simpleSleep";
|
||||||
|
public static final String PROCESS_NAME_SIMPLE_THROW = "simpleThrow";
|
||||||
|
public static final String PROCESS_NAME_SLEEP_INTERACTIVE = "sleepInteractive";
|
||||||
|
|
||||||
|
public static final String STEP_NAME_SLEEPER = "sleeper";
|
||||||
|
public static final String STEP_NAME_THROWER = "thrower";
|
||||||
|
|
||||||
|
public static final String SCREEN_0 = "screen0";
|
||||||
|
public static final String SCREEN_1 = "screen1";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Prime a test database (e.g., h2, in-memory)
|
** Prime a test database (e.g., h2, in-memory)
|
||||||
@ -101,6 +117,10 @@ public class TestUtils
|
|||||||
qInstance.addBackend(defineBackend());
|
qInstance.addBackend(defineBackend());
|
||||||
qInstance.addTable(defineTablePerson());
|
qInstance.addTable(defineTablePerson());
|
||||||
qInstance.addProcess(defineProcessGreetPeople());
|
qInstance.addProcess(defineProcessGreetPeople());
|
||||||
|
qInstance.addProcess(defineProcessGreetPeopleInteractive());
|
||||||
|
qInstance.addProcess(defineProcessSimpleSleep());
|
||||||
|
qInstance.addProcess(defineProcessScreenThenSleep());
|
||||||
|
qInstance.addProcess(defineProcessSimpleThrow());
|
||||||
return (qInstance);
|
return (qInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,12 +187,12 @@ public class TestUtils
|
|||||||
return new QProcessMetaData()
|
return new QProcessMetaData()
|
||||||
.withName("greet")
|
.withName("greet")
|
||||||
.withTableName("person")
|
.withTableName("person")
|
||||||
.addFunction(new QFunctionMetaData()
|
.addStep(new QBackendStepMetaData()
|
||||||
.withName("prepare")
|
.withName("prepare")
|
||||||
.withCode(new QCodeReference()
|
.withCode(new QCodeReference()
|
||||||
.withName(MockFunctionBody.class.getName())
|
.withName(MockBackendStep.class.getName())
|
||||||
.withCodeType(QCodeType.JAVA)
|
.withCodeType(QCodeType.JAVA)
|
||||||
.withCodeUsage(QCodeUsage.FUNCTION)) // todo - needed, or implied in this context?
|
.withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context?
|
||||||
.withInputData(new QFunctionInputMetaData()
|
.withInputData(new QFunctionInputMetaData()
|
||||||
.withRecordListMetaData(new QRecordListMetaData().withTableName("person"))
|
.withRecordListMetaData(new QRecordListMetaData().withTableName("person"))
|
||||||
.withFieldList(List.of(
|
.withFieldList(List.of(
|
||||||
@ -185,10 +205,198 @@ public class TestUtils
|
|||||||
.addField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
|
.addField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
|
||||||
)
|
)
|
||||||
.withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))
|
.withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))
|
||||||
.withOutputView(new QOutputView()
|
|
||||||
.withMessageField("outputMessage")
|
|
||||||
.withRecordListView(new QRecordListView().withFieldNames(List.of("id", "firstName", "lastName", "fullGreeting"))))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Define an interactive version of the 'greet people' process
|
||||||
|
*******************************************************************************/
|
||||||
|
private static QProcessMetaData defineProcessGreetPeopleInteractive()
|
||||||
|
{
|
||||||
|
return new QProcessMetaData()
|
||||||
|
.withName(PROCESS_NAME_GREET_PEOPLE_INTERACTIVE)
|
||||||
|
.withTableName("person")
|
||||||
|
|
||||||
|
.addStep(new QFrontendStepMetaData()
|
||||||
|
.withName("setup")
|
||||||
|
.withFormField(new QFieldMetaData("greetingPrefix", QFieldType.STRING))
|
||||||
|
.withFormField(new QFieldMetaData("greetingSuffix", QFieldType.STRING))
|
||||||
|
)
|
||||||
|
|
||||||
|
.addStep(new QBackendStepMetaData()
|
||||||
|
.withName("doWork")
|
||||||
|
.withCode(new QCodeReference()
|
||||||
|
.withName(MockBackendStep.class.getName())
|
||||||
|
.withCodeType(QCodeType.JAVA)
|
||||||
|
.withCodeUsage(QCodeUsage.BACKEND_STEP)) // todo - needed, or implied in this context?
|
||||||
|
.withInputData(new QFunctionInputMetaData()
|
||||||
|
.withRecordListMetaData(new QRecordListMetaData().withTableName("person"))
|
||||||
|
.withFieldList(List.of(
|
||||||
|
new QFieldMetaData("greetingPrefix", QFieldType.STRING),
|
||||||
|
new QFieldMetaData("greetingSuffix", QFieldType.STRING)
|
||||||
|
)))
|
||||||
|
.withOutputMetaData(new QFunctionOutputMetaData()
|
||||||
|
.withRecordListMetaData(new QRecordListMetaData()
|
||||||
|
.withTableName("person")
|
||||||
|
.addField(new QFieldMetaData("fullGreeting", QFieldType.STRING))
|
||||||
|
)
|
||||||
|
.withFieldList(List.of(new QFieldMetaData("outputMessage", QFieldType.STRING))))
|
||||||
|
)
|
||||||
|
|
||||||
|
.addStep(new QFrontendStepMetaData()
|
||||||
|
.withName("results")
|
||||||
|
.withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Define a process with just one step that sleeps
|
||||||
|
*******************************************************************************/
|
||||||
|
private static QProcessMetaData defineProcessSimpleSleep()
|
||||||
|
{
|
||||||
|
return new QProcessMetaData()
|
||||||
|
.withName(PROCESS_NAME_SIMPLE_SLEEP)
|
||||||
|
.withIsHidden(true)
|
||||||
|
.addStep(SleeperStep.getMetaData());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Define a process with a screen, then a sleep step
|
||||||
|
*******************************************************************************/
|
||||||
|
private static QProcessMetaData defineProcessScreenThenSleep()
|
||||||
|
{
|
||||||
|
return new QProcessMetaData()
|
||||||
|
.withName(PROCESS_NAME_SLEEP_INTERACTIVE)
|
||||||
|
.addStep(new QFrontendStepMetaData()
|
||||||
|
.withName(SCREEN_0)
|
||||||
|
.withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING)))
|
||||||
|
.addStep(SleeperStep.getMetaData())
|
||||||
|
.addStep(new QFrontendStepMetaData()
|
||||||
|
.withName(SCREEN_1)
|
||||||
|
.withFormField(new QFieldMetaData("outputMessage", QFieldType.STRING)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Define a process with just one step that sleeps and then throws
|
||||||
|
*******************************************************************************/
|
||||||
|
private static QProcessMetaData defineProcessSimpleThrow()
|
||||||
|
{
|
||||||
|
return new QProcessMetaData()
|
||||||
|
.withName(PROCESS_NAME_SIMPLE_THROW)
|
||||||
|
.addStep(ThrowerStep.getMetaData());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Testing backend step - just sleeps however long you ask it to (or, throws if
|
||||||
|
** you don't provide a number of seconds to sleep).
|
||||||
|
*******************************************************************************/
|
||||||
|
public static class SleeperStep implements BackendStep
|
||||||
|
{
|
||||||
|
public static final String FIELD_SLEEP_MILLIS = "sleepMillis";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Execute the backend step - using the request as input, and the result as output.
|
||||||
|
**
|
||||||
|
******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void run(RunBackendStepRequest runBackendStepRequest, RunBackendStepResult runBackendStepResult) throws QException
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Thread.sleep(runBackendStepRequest.getValueInteger(FIELD_SLEEP_MILLIS));
|
||||||
|
}
|
||||||
|
catch(InterruptedException e)
|
||||||
|
{
|
||||||
|
throw (new QException("Interrupted while sleeping..."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static QBackendStepMetaData getMetaData()
|
||||||
|
{
|
||||||
|
return (new QBackendStepMetaData()
|
||||||
|
.withName(STEP_NAME_SLEEPER)
|
||||||
|
.withCode(new QCodeReference()
|
||||||
|
.withName(SleeperStep.class.getName())
|
||||||
|
.withCodeType(QCodeType.JAVA)
|
||||||
|
.withCodeUsage(QCodeUsage.BACKEND_STEP))
|
||||||
|
.withInputData(new QFunctionInputMetaData()
|
||||||
|
.addField(new QFieldMetaData(SleeperStep.FIELD_SLEEP_MILLIS, QFieldType.INTEGER))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Testing backend step - just throws an exception after however long you ask it to sleep.
|
||||||
|
*******************************************************************************/
|
||||||
|
public static class ThrowerStep implements BackendStep
|
||||||
|
{
|
||||||
|
public static final String FIELD_SLEEP_MILLIS = "sleepMillis";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Execute the backend step - using the request as input, and the result as output.
|
||||||
|
**
|
||||||
|
******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void run(RunBackendStepRequest runBackendStepRequest, RunBackendStepResult runBackendStepResult) throws QException
|
||||||
|
{
|
||||||
|
int sleepMillis;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sleepMillis = runBackendStepRequest.getValueInteger(FIELD_SLEEP_MILLIS);
|
||||||
|
}
|
||||||
|
catch(QValueException qve)
|
||||||
|
{
|
||||||
|
sleepMillis = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Thread.sleep(sleepMillis);
|
||||||
|
}
|
||||||
|
catch(InterruptedException e)
|
||||||
|
{
|
||||||
|
throw (new QException("Interrupted while sleeping..."));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw (new QException("I always throw."));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static QBackendStepMetaData getMetaData()
|
||||||
|
{
|
||||||
|
return (new QBackendStepMetaData()
|
||||||
|
.withName(STEP_NAME_THROWER)
|
||||||
|
.withCode(new QCodeReference()
|
||||||
|
.withName(ThrowerStep.class.getName())
|
||||||
|
.withCodeType(QCodeType.JAVA)
|
||||||
|
.withCodeUsage(QCodeUsage.BACKEND_STEP))
|
||||||
|
.withInputData(new QFunctionInputMetaData()
|
||||||
|
.addField(new QFieldMetaData(ThrowerStep.FIELD_SLEEP_MILLIS, QFieldType.INTEGER))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user