mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-20 22:18:43 +00:00
Compare commits
6 Commits
snapshot-f
...
snapshot-i
Author | SHA1 | Date | |
---|---|---|---|
ecc925715b | |||
0c4dc1d4cb | |||
3354717fd6 | |||
1019218762 | |||
cb382c3f4b | |||
af757ea1fd |
@ -98,31 +98,6 @@ commands:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "pom.xml" }}
|
||||
|
||||
install_asciidoctor:
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install asciidoctor
|
||||
command: |
|
||||
sudo apt-get update
|
||||
sudo apt install -y asciidoctor
|
||||
|
||||
run_asciidoctor:
|
||||
steps:
|
||||
- run:
|
||||
name: Run asciidoctor
|
||||
command: |
|
||||
cd docs
|
||||
asciidoctor -a docinfo=shared index.adoc
|
||||
|
||||
upload_docs_site:
|
||||
steps:
|
||||
- run:
|
||||
name: scp html to justinsgotskinnylegs.com
|
||||
command: |
|
||||
cd docs
|
||||
scp index.html dkelkhoff@45.79.44.221:/mnt/first-volume/dkelkhoff/nginx/html/justinsgotskinnylegs.com/qqq-docs.html
|
||||
|
||||
jobs:
|
||||
mvn_test:
|
||||
executor: localstack/default
|
||||
@ -139,13 +114,6 @@ jobs:
|
||||
- mvn_verify
|
||||
- mvn_jar_deploy
|
||||
|
||||
publish_asciidoc:
|
||||
executor: localstack/default
|
||||
steps:
|
||||
- install_asciidoctor
|
||||
- run_asciidoctor
|
||||
- upload_docs_site
|
||||
|
||||
workflows:
|
||||
test_only:
|
||||
jobs:
|
||||
@ -153,7 +121,7 @@ workflows:
|
||||
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(dev|integration.*)/
|
||||
ignore: /dev/
|
||||
tags:
|
||||
ignore: /(version|snapshot)-.*/
|
||||
|
||||
@ -163,10 +131,7 @@ workflows:
|
||||
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
|
||||
filters:
|
||||
branches:
|
||||
only: /(dev|integration.*)/
|
||||
only: /dev/
|
||||
tags:
|
||||
only: /(version|snapshot)-.*/
|
||||
- publish_asciidoc:
|
||||
filters:
|
||||
branches:
|
||||
only: /dev/
|
||||
|
||||
|
@ -21,99 +21,5 @@ Used to set values in the `displayValues` map within a `QRecord`.
|
||||
* `possibleValueSourceName` - *String* - Reference to a {link-pvs} to be used for this field.
|
||||
Values in this field should correspond to ids from the referenced Possible Value Source.
|
||||
* `maxLength` - *Integer* - Maximum length (number of characters) allowed for values in this field.
|
||||
Only applicable for fields with `type=STRING`. Needs to be used with a `FieldBehavior` of type `ValueTooLongBehavior`.
|
||||
|
||||
==== Field Behaviors
|
||||
Additional behaviors can be attached to fields through the use of the `behaviors` attribute,
|
||||
which is a `Set` of 0 or more instances of implementations of the `FieldBehavior` interface.
|
||||
Note that in some cases, these instances may be `enum` constants,
|
||||
but other times may be regular Objects.
|
||||
|
||||
QQQ provides a set of common field behaviors.
|
||||
Applications can also define their own field behaviors by implementing the `FieldBehavior` interface,
|
||||
and attaching instances of their custom behavior classes to fields.
|
||||
|
||||
===== ValueTooLongBehavior
|
||||
Used on String fields. Requires the field to have a `maxLength` set.
|
||||
Depending on the chosen instance of this enum, before a record is Inserted or Updated,
|
||||
if the value in the field is longer than the `maxLength`, then one of the following actions can occur:
|
||||
|
||||
* `TRUNCATE` - The value will be simply truncated to the `maxLength`.
|
||||
* `TRUNCATE_ELLIPSIS` - The value will be truncated to 3 characters less than the `maxLength`, and three periods (an ellipsis) will be placed at the end.
|
||||
* `ERROR` - An error will be reported, and the record will not be inserted or updated.
|
||||
* `PASS_THROUGH` - Nothing will happen. This is the same as not having a `ValueTooLongBehavior` on the field.
|
||||
|
||||
[source,java]
|
||||
.Examples of using ValueTooLongBehavior
|
||||
----
|
||||
new QFieldMetaData("sku", QFieldType.STRING)
|
||||
.withMaxLength(40),
|
||||
.withBehavior(ValueTooLongBehavior.ERROR),
|
||||
|
||||
new QFieldMetaData("reason", QFieldType.STRING)
|
||||
.withMaxLength(250),
|
||||
.withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS),
|
||||
|
||||
----
|
||||
|
||||
===== DynamicDefaultValueBehavior
|
||||
Used to set a dynamic default value to a field when it is being inserted or updated.
|
||||
For example, instead of having a hard-coded `defaultValue` specified in the field meta-data,
|
||||
and instead of having to add, for example, a pre-insert custom action.
|
||||
|
||||
* `CREATE_DATE` - On inserts, sets the field's value to the current time.
|
||||
* `MODIFY_DATE` - On inserts and updates, sets the field's value to the current time.
|
||||
* `USER_ID` - On inserts and updates, sets the field's value to the current user's id (but only if the value is currently null).
|
||||
|
||||
_Note that the `QInstanceEnricher` will, by default, add the `CREATE_DATE` and `MODIFY_DATE` `DynamicDefaultValueBehavior`
|
||||
options to any fields named `"createDate"` or `"modifyDate"`.
|
||||
This behavior can be disabled by setting the `configAddDynamicDefaultValuesToFieldsNamedCreateDateAndModifyDate` property
|
||||
on the `QInstanceEnricher` instance used by the application to `false`._
|
||||
|
||||
[source,java]
|
||||
.Examples of using DynamicDefaultValueBehavior
|
||||
----
|
||||
new QFieldMetaData("createDate", QFieldType.DATE_TIME)
|
||||
.withBehavior(DynamicDefaultValueBehavior.CREATE_DATE),
|
||||
|
||||
new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)
|
||||
.withBehavior(DynamicDefaultValueBehavior.MODIFY_DATE),
|
||||
|
||||
new QFieldMetaData("createdByUserId", QFieldType.STRING)
|
||||
.withBehavior(DynamicDefaultValueBehavior.USER_ID),
|
||||
----
|
||||
|
||||
===== DateTimeDisplayValueBehavior
|
||||
By default, in QQQ, fields of type `DATE_TIME` are stored in UTC,
|
||||
and their values in a QRecord is a java `Instant` instance, which is always UTC.
|
||||
However, frontends will prefer to display date-time values in the user's local Time Zone whenever possible.
|
||||
|
||||
Using `DateTimeDisplayValueBehavior` allows a `DATE_TIME` field to be displayed in a different Time Zone.
|
||||
An example use-case for this would be displaying airplane flight times,
|
||||
where you would want a flight from California to New York to display Pacific Time for its departure time,
|
||||
and Eastern Time for its arrival.
|
||||
|
||||
An instance of `DateTimeDisplayValueBehavior` can be configured to either use a hard-coded time `ZoneId`
|
||||
(for example, to always show users UTC, or a business's home-office time zone).
|
||||
Or, it can be set up to get the time zone to use from another field in the table.
|
||||
|
||||
[source,java]
|
||||
.Examples of using DateTimeDisplayValueBehavior
|
||||
----
|
||||
new QTableMetaData().withName("flights").withFields(List.of(
|
||||
...
|
||||
new QFieldMetaData("departureTimeZoneId", QFieldType.STRING),
|
||||
new QFieldMetaData("arrivaTimeZoneId", QFieldType.STRING),
|
||||
|
||||
new QFieldMetaData("departureTime", QFieldType.DATE_TIME)
|
||||
.withBehavior(new DateTimeDisplayValueBehavior()
|
||||
.withZoneIdFromFieldName("departureTimeZoneId")),
|
||||
|
||||
new QFieldMetaData("arrivalTime", QFieldType.DATE_TIME)
|
||||
.withBehavior(new DateTimeDisplayValueBehavior()
|
||||
.withZoneIdFromFieldName("arrivalTimeZoneId"))
|
||||
|
||||
new QFieldMetaData("ticketSaleStartDateTime", QFieldType.DATE_TIME)
|
||||
.withBehavior(new DateTimeDisplayValueBehavior()
|
||||
.withDefaultZoneId("UTC"))
|
||||
----
|
||||
Only applicable for fields with `type=STRING`.
|
||||
* `
|
@ -2,57 +2,16 @@
|
||||
== Joins
|
||||
include::../variables.adoc[]
|
||||
|
||||
A `QJoinMetaData` is a meta-data object that tells QQQ, essentially “it is possible for these 2 tables to join, here’s how to do it”.
|
||||
|
||||
Joins can be used then, in an application, in a number of possible ways:
|
||||
|
||||
* In a {link-table}, we can specify joins to be “exposed”, e.g., made available to users on a query screen
|
||||
* Also in a Table, as part of an “Association”, which sets up one table as a “parent” of another,
|
||||
such that you can store (and fetch) the child-records at the same time as the parent
|
||||
** A common use-case here may be an order & lineItem table -
|
||||
such that QQQ can generate an API uses to allow you to post an order and its lines in a single request, and they get stored all together
|
||||
* In defining the security field (record lock) on a table,
|
||||
sometimes, it isn’t a field directly on the table, but instead comes from a joined table (possibly even more than just 1 table away).
|
||||
** For example, maybe a lineItem table, doesn't have a clientId, but needs secured by that field
|
||||
- so its recordLock can specify a “joinNameChain” that describes how to get from lineItem to order.clientId
|
||||
* The `QueryAction` can take (through its QueryInput object) zero or more QueryJoin objects,
|
||||
which must make a reference (implicitly or explicitly) to a QJoinMetaData.
|
||||
See the section on <<QueryJoin,QueryJoins>> for more details.
|
||||
#TODO#
|
||||
|
||||
=== QJoinMetaData
|
||||
Joins are defined in a QQQ Instance in a `*QJoinMetaData*` object.
|
||||
|
||||
In this object, we have the concept of a "leftTable" and a "rightTable".
|
||||
There isn't generally anything special about which table is on the "left" and which is on the "right".
|
||||
But the remaining pieces of the QJoinMetaData do all need to line-up with these sides.
|
||||
|
||||
For example:
|
||||
|
||||
* The Type (one-to-one, one-to-many, many-to-one) - where the leftTable comes first, and rightTable comes second
|
||||
(e.g., a one-to-many means 1-row in leftTable has many-rows in rightTable associated with it)
|
||||
* In a JoinOn object, the 1st field name given is from the leftTable;
|
||||
the second fieldName from the rightTable.
|
||||
#TODO#
|
||||
|
||||
*QJoinMetaData Properties:*
|
||||
|
||||
* `name` - *String, Required* - Unique name for the join within the QQQ Instance.
|
||||
** One convention is to name joins based on (leftTable + "Join" + rightTable).
|
||||
** If you do not wish to define join names yourself, the method `withInferredName()`
|
||||
can be called (which defers to
|
||||
`public static String makeInferredJoinName(String leftTable, String rightTable)`),
|
||||
to create a name for the join following the (leftTable + "Join" + rightTable) convention.
|
||||
* `leftTable` - *String, Required* - Name of a {link-table} in the {link-instance}.
|
||||
* `rightTable` - *String, Required* - Name of a {link-table} in the {link-instance}.
|
||||
* `type` - *enum, Required* - cardinality between the two tables in the join.
|
||||
** e.g., `ONE_TO_ONE`, `ONE_TO_MANY` (indicating 1 record in the left table may join
|
||||
to many records in the right table), or `MANY_TO_ONE` (vice-versa).
|
||||
** Note that there is no MANY_TO_MANY option, as a many-to-many is built as multiple QJoinMetaData's
|
||||
going through the intermediary (intersection) table.
|
||||
* `joinOns` - *List<JoinOn>, Required* - fields used to join the tables.
|
||||
Note: In the 2-arg JoinOn constructor, the leftTable's field comes first.
|
||||
Alternatively, the no-arg constructor can be used along with `.withLeftField().withRightField()`
|
||||
* `orderBys` - *List<QFilterOrderBy>* - Optional list of order-by objects,
|
||||
used in some framework-generated queries using the join.
|
||||
The field names are assumed to come from the rightTable.
|
||||
* `name` - *String, Required* - Unique name for the join within the QQQ Instance. #todo infererences or conventions?#
|
||||
|
||||
#TODO#
|
||||
|
||||
#TODO# what else do we need here?
|
||||
|
@ -16,7 +16,6 @@ Processes are defined in a QQQ Instance in a `*QProcessMetaData*` object.
|
||||
In addition to directly building a `QProcessMetaData` object setting its properties directly, there are a few common process patterns that provide *Builder* objects for ease-of-use.
|
||||
See StreamedETLWithFrontendProcess below for a common example
|
||||
|
||||
[#_QProcessMetaData_Properties]
|
||||
*QProcessMetaData Properties:*
|
||||
|
||||
* `name` - *String, Required* - Unique name for the process within the QQQ Instance.
|
||||
@ -31,13 +30,12 @@ See below for details.
|
||||
* `permissionRules` - *QPermissionRules object* - define the permission/access rules for the process.
|
||||
See {link-permissionRules} for details.
|
||||
* `steps` and `stepList` - *Map of String → <<QStepMetaData>>* and *List of QStepMetaData* - Defines the <<QFrontendStepMetaData,screens>> and <<QBackendStepMetaData,backend code>> that makes up the process.
|
||||
** `stepList` is the list of steps in the order that they will be executed
|
||||
(that is to say - this is the _default_ order of execution - but it can be customized - see <<_custom_process_flow>> for details).
|
||||
** `steps` is a map, including all steps from `stepList`, but which may also include steps which can used by the process if its backend steps make the decision to do so, at run-time (e.g., using <<_custom_process_flow>>).
|
||||
** `stepList` is the list of steps in the order that they will by default be executed.
|
||||
** `steps` is a map, including all steps from `stepList`, but which may also include steps which can used by the process if its backend steps make the decision to do so, at run-time.
|
||||
** A process's steps are normally defined in one of two was:
|
||||
*** 1) by a single call to `.withStepList(List<QStepMetaData>)`, which internally adds each step into the `steps` map.
|
||||
*** 2) by multiple calls to `.addStep(QStepMetaData)`, which adds a step to both the `stepList` and `steps` map.
|
||||
** If a process also needs optional steps (for a <<_custom_process_flow>>), they should be added by a call to `.addOptionalStep(QStepMetaData)`, which only places them in the `steps` map.
|
||||
** If a process also needs optional steps, they should be added by a call to `.addOptionalStep(QStepMetaData)`, which only places them in the `steps` map.
|
||||
* `schedule` - *<<QScheduleMetaData>>* - set up the process to run automatically on the specified schedule.
|
||||
See below for details.
|
||||
* `minInputRecords` - *Integer* - #not used...#
|
||||
@ -216,112 +214,3 @@ But for some cases, doing page-level transactions can reduce long-transactions a
|
||||
* `withFields(List<QFieldMetaData> fieldList)` - Adds additional input fields to the preview step of the process.
|
||||
* `withBasepullConfiguration(BasepullConfiguration basepullConfiguration)` - Add a <<BasepullConfiguration>> to the process.
|
||||
* `withSchedule(QScheduleMetaData schedule)` - Add a <<QScheduleMetaData>> to the process.
|
||||
|
||||
[#_custom_process_flow]
|
||||
==== Custom Process Flow
|
||||
As referenced in the definition of the <<_QProcessMetaData_Properties,QProcessMetaData Properties>>, by default, a process
|
||||
will execute each of its steps in-order, as defined in the `stepList` property.
|
||||
However, a Backend Step can customize this flow #todo - write more clearly here...
|
||||
|
||||
There are generally 2 method to call (in a `BackendStep`) to do a dynamic flow:
|
||||
|
||||
* `RunBackendStepOutput.setOverrideLastStepName(String stepName)`
|
||||
** QQQ's `RunProcessAction` keeps track of which step it "last" ran, e.g., to tell it which one to run next.
|
||||
However, if a step sets the `OverrideLastStepName` property in its output object,
|
||||
then the step named in that property becomes the effective "last" step,
|
||||
thus determining which step comes next.
|
||||
|
||||
* `RunBackendStepOutput.updateStepList(List<String> stepNameList)`
|
||||
** Calling this method changes the process's runtime definition of steps to be executed.
|
||||
Thus allowing a completely custom flow.
|
||||
It should be noted, that the "last" step name (as tracked by QQQ within `RunProcessAction`)
|
||||
does need to be found in the new `stepNameList` - otherwise, the framework will not know where you were,
|
||||
for figuring out where to go next.
|
||||
|
||||
[source,java]
|
||||
.Example of a defining process that can use a flexible flow:
|
||||
----
|
||||
// for a case like this, it would be recommended to define all step names in constants:
|
||||
public final static String STEP_START = "start";
|
||||
public final static String STEP_A = "a";
|
||||
public final static String STEP_B = "b";
|
||||
public final static String STEP_C = "c";
|
||||
public final static String STEP_1 = "1";
|
||||
public final static String STEP_2 = "2";
|
||||
public final static String STEP_3 = "3";
|
||||
public final static String STEP_END = "end";
|
||||
|
||||
// also, to define the possible flows (lists of steps) in constants as well:
|
||||
public final static List<String> LETTERS_STEP_LIST = List.of(
|
||||
STEP_START, STEP_A, STEP_B, STEP_C, STEP_END);
|
||||
|
||||
public final static List<String> NUMBERS_STEP_LIST = List.of(
|
||||
STEP_START, STEP_1, STEP_2, STEP_3, STEP_END);
|
||||
|
||||
// when we define the process's meta-data, we only give a "skeleton" stepList -
|
||||
// we must at least have our starting step, and we may want at least one frontend step
|
||||
// for the UI to show some placeholder(s):
|
||||
QProcessMetaData process = new QProcessMetaData()
|
||||
.withName(PROCESS_NAME)
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData().withName(STEP_START)
|
||||
.withCode(new QCodeReference(/*...*/)),
|
||||
new QFrontendStepMetaData()
|
||||
.withName(STEP_END)
|
||||
));
|
||||
|
||||
// the additional steps get added via `addOptionalStep`, which only puts them in
|
||||
// the process's stepMap, not its stepList!
|
||||
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_A));
|
||||
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_B)
|
||||
.withCode(new QCodeReference(/*...*/)));
|
||||
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_C));
|
||||
|
||||
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_1)
|
||||
.withCode(new QCodeReference(/*...*/)));
|
||||
process.addOptionalStep(new QFrontendStepMetaData().withName(STEP_2));
|
||||
process.addOptionalStep(new QBackendStepMetaData().withName(STEP_3)
|
||||
.withCode(new QCodeReference(/*...*/)));
|
||||
|
||||
----
|
||||
|
||||
[source,java]
|
||||
.Example of a process backend step adjusting the process's runtime flow:
|
||||
----
|
||||
/***************************************************************************
|
||||
** look at the value named "which". if it's "letters", then make the process
|
||||
** go through the stepList consisting of letters; else, update the step list
|
||||
** to be the "numbers" steps.
|
||||
**
|
||||
** Also - if the "skipSomeSteps" value is give as true, then set the
|
||||
** overrideLastStepName to skip again (in the letters case, skip past A, B
|
||||
** and C; in the numbers case, skip past 1 and 2).
|
||||
***************************************************************************/
|
||||
public static class StartStep implements BackendStep
|
||||
{
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
Boolean skipSomeSteps = runBackendStepInput.getValueBoolean("skipSomeSteps");
|
||||
|
||||
if(runBackendStepInput.getValueString("which").equals("letters"))
|
||||
{
|
||||
runBackendStepOutput.updateStepList(LETTERS_STEP_LIST);
|
||||
if(BooleanUtils.isTrue(skipSomeSteps))
|
||||
{
|
||||
runBackendStepOutput.setOverrideLastStepName(STEP_C);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
runBackendStepOutput.updateStepList(NUMBERS_STEP_LIST);
|
||||
if(BooleanUtils.isTrue(skipSomeSteps))
|
||||
{
|
||||
runBackendStepOutput.setOverrideLastStepName(STEP_2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
9
pom.xml
9
pom.xml
@ -29,7 +29,6 @@
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>qqq-bom</module>
|
||||
<module>qqq-backend-core</module>
|
||||
<module>qqq-backend-module-api</module>
|
||||
<module>qqq-backend-module-filesystem</module>
|
||||
@ -55,7 +54,7 @@
|
||||
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
|
||||
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
|
||||
<coverage.haltOnFailure>true</coverage.haltOnFailure>
|
||||
<coverage.instructionCoveredRatioMinimum>0.75</coverage.instructionCoveredRatioMinimum>
|
||||
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
|
||||
<coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum>
|
||||
<plugin.shade.phase>none</plugin.shade.phase>
|
||||
</properties>
|
||||
@ -246,7 +245,8 @@ echo " See also target/site/jacoco/index.html"
|
||||
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
|
||||
echo "------------------------------------------------------------"
|
||||
|
||||
if which xpath > /dev/null 2>&1; then
|
||||
which xpath > /dev/null 2>&1
|
||||
if [ "$?" == "0" ]; then
|
||||
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 -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
|
||||
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
|
||||
@ -255,7 +255,8 @@ else
|
||||
echo "xpath is not installed. Jacoco coverage summary will not be produced here...";
|
||||
fi
|
||||
|
||||
if which html2text > /dev/null 2>&1; then
|
||||
which xpath > /dev/null 2>&1
|
||||
if [ "$?" == "0" ]; then
|
||||
echo "Untested classes, per Jacoco:"
|
||||
echo "-----------------------------"
|
||||
for i in target/site/jacoco/*/index.html; do
|
||||
|
@ -102,16 +102,6 @@
|
||||
<artifactId>fastexcel</artifactId>
|
||||
<version>0.12.15</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<version>5.2.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.2.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>auth0</artifactId>
|
||||
@ -173,19 +163,6 @@
|
||||
<version>1.12.321</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-ses</artifactId>
|
||||
<version>1.12.705</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cloud.localstack</groupId>
|
||||
<artifactId>localstack-utils</artifactId>
|
||||
<version>0.2.20</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.quartz-scheduler</groupId>
|
||||
<artifactId>quartz</artifactId>
|
||||
@ -199,12 +176,6 @@
|
||||
<version>2.23.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>jakarta.mail</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Common deps for all qqq modules -->
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
@ -169,24 +169,17 @@ public class AsyncJobManager
|
||||
LOG.debug("Completed job " + uuidAndTypeStateKey.getUuid());
|
||||
return (result);
|
||||
}
|
||||
catch(Throwable t)
|
||||
catch(Exception e)
|
||||
{
|
||||
asyncJobStatus.setState(AsyncJobState.ERROR);
|
||||
if(t instanceof Exception e)
|
||||
{
|
||||
asyncJobStatus.setCaughtException(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
asyncJobStatus.setCaughtException(new QException("Caught throwable", t));
|
||||
}
|
||||
asyncJobStatus.setCaughtException(e);
|
||||
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// if user facing, just log an info, warn otherwise //
|
||||
//////////////////////////////////////////////////////
|
||||
LOG.log((t instanceof QUserFacingException) ? Level.INFO : Level.WARN, "Job ended with an exception", t, logPair("jobId", uuidAndTypeStateKey.getUuid()));
|
||||
throw (new CompletionException(t));
|
||||
LOG.log((e instanceof QUserFacingException) ? Level.INFO : Level.WARN, "Job ended with an exception", e, logPair("jobId", uuidAndTypeStateKey.getUuid()));
|
||||
throw (new CompletionException(e));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -31,7 +31,6 @@ import java.io.Serializable;
|
||||
*******************************************************************************/
|
||||
public class AsyncJobStatus implements Serializable
|
||||
{
|
||||
private String jobName;
|
||||
private AsyncJobState state;
|
||||
private String message;
|
||||
private Integer current;
|
||||
@ -188,36 +187,4 @@ public class AsyncJobStatus implements Serializable
|
||||
{
|
||||
this.cancelRequested = cancelRequested;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for jobName
|
||||
*******************************************************************************/
|
||||
public String getJobName()
|
||||
{
|
||||
return (this.jobName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for jobName
|
||||
*******************************************************************************/
|
||||
public void setJobName(String jobName)
|
||||
{
|
||||
this.jobName = jobName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for jobName
|
||||
*******************************************************************************/
|
||||
public AsyncJobStatus withJobName(String jobName)
|
||||
{
|
||||
this.jobName = jobName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.async;
|
||||
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** subclass designed to be used when we want there to be an instance (so code
|
||||
** doesn't have to all be null-tolerant), but there's no one who will ever be
|
||||
** reading the status data, so we don't need to store the object in a
|
||||
** state provider.
|
||||
*******************************************************************************/
|
||||
public class NonPersistedAsyncJobCallback extends AsyncJobCallback
|
||||
{
|
||||
private final AsyncJobStatus asyncJobStatus;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public NonPersistedAsyncJobCallback(UUID jobUUID, AsyncJobStatus asyncJobStatus)
|
||||
{
|
||||
super(jobUUID, asyncJobStatus);
|
||||
this.asyncJobStatus = asyncJobStatus;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected void storeUpdatedStatus()
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// noop - cf. base class, which writes to persistence here (our point is, we do not) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
|
||||
}
|
@ -149,7 +149,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
// sort the field names by their labels //
|
||||
//////////////////////////////////////////
|
||||
List<String> sortedFieldNames = table.getFields().keySet().stream()
|
||||
.sorted(Comparator.comparing(fieldName -> Objects.requireNonNullElse(table.getFields().get(fieldName).getLabel(), fieldName)))
|
||||
.sorted(Comparator.comparing(fieldName -> table.getFields().get(fieldName).getLabel()))
|
||||
.toList();
|
||||
|
||||
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
||||
@ -303,7 +303,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
}
|
||||
else
|
||||
{
|
||||
String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
|
||||
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
@ -329,8 +329,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
}
|
||||
else
|
||||
{
|
||||
String formattedValue = getFormattedValueForAuditDetail(table, record, fieldName, field, value);
|
||||
String formattedOldValue = getFormattedValueForAuditDetail(table, oldRecord, fieldName, field, oldValue);
|
||||
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
|
||||
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
|
||||
|
||||
if(oldValue == null)
|
||||
{
|
||||
@ -464,7 +464,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static String getFormattedValueForAuditDetail(QTableMetaData table, QRecord record, String fieldName, QFieldMetaData field, Serializable value)
|
||||
private static String getFormattedValueForAuditDetail(QRecord record, String fieldName, QFieldMetaData field, Serializable value)
|
||||
{
|
||||
String formattedValue = null;
|
||||
if(value != null)
|
||||
@ -479,8 +479,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
}
|
||||
else
|
||||
{
|
||||
QValueFormatter.setDisplayValuesInRecord(table, table.getFields(), record);
|
||||
formattedValue = record.getDisplayValue(fieldName);
|
||||
formattedValue = QValueFormatter.formatValue(field, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -526,7 +526,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
|
||||
LOG.debug("Of the [" + records.size() + "] records that were pending automations, [" + matchingQRecords.size() + "] of them match the filter on the action:" + action);
|
||||
LOG.debug("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
|
||||
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
|
||||
{
|
||||
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
|
||||
@ -601,7 +601,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
|
||||
/*******************************************************************************
|
||||
** Finally, actually run action code against a list of known matching records.
|
||||
**
|
||||
** todo not commit - move to somewhere genericer
|
||||
*******************************************************************************/
|
||||
public static void applyActionToMatchingRecords(QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws Exception
|
||||
{
|
||||
|
@ -96,7 +96,7 @@ public class QCodeLoader
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
|
||||
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
|
||||
@ -135,7 +135,7 @@ public class QCodeLoader
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
|
||||
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
|
||||
@ -187,7 +187,7 @@ public class QCodeLoader
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error initializing customizer", e, logPair("codeReference", codeReference));
|
||||
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
|
||||
|
@ -42,7 +42,6 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -73,104 +72,80 @@ public abstract class AbstractWidgetRenderer
|
||||
*******************************************************************************/
|
||||
protected boolean setupDropdowns(RenderWidgetInput input, QWidgetMetaData metaData, QWidgetData widgetData) throws QException
|
||||
{
|
||||
List<List<Map<String, String>>> dataList = new ArrayList<>();
|
||||
List<String> labelList = new ArrayList<>();
|
||||
List<String> nameList = new ArrayList<>();
|
||||
List<List<Map<String, String>>> pvsData = new ArrayList<>();
|
||||
List<String> pvsLabels = new ArrayList<>();
|
||||
List<String> pvsNames = new ArrayList<>();
|
||||
List<String> missingRequiredSelections = new ArrayList<>();
|
||||
for(WidgetDropdownData dropdownData : CollectionUtils.nonNullList(metaData.getDropdowns()))
|
||||
{
|
||||
if(WidgetDropdownType.DATE_PICKER.equals(dropdownData.getType()))
|
||||
{
|
||||
String name = dropdownData.getName();
|
||||
nameList.add(name);
|
||||
labelList.add(dropdownData.getLabel());
|
||||
dataList.add(new ArrayList<>());
|
||||
String possibleValueSourceName = dropdownData.getPossibleValueSourceName();
|
||||
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// sure that something has been selected, and if not, display a message that a selection needs made //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(dropdownData.getIsRequired())
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this looks complicated, but is just look for a label in the dropdown data and if found use it, //
|
||||
// otherwise look for label in PVS and if found use that, otherwise just use the PVS name //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String pvsLabel = dropdownData.getLabel() != null ? dropdownData.getLabel() : (possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName);
|
||||
pvsLabels.add(pvsLabel);
|
||||
pvsNames.add(possibleValueSourceName);
|
||||
|
||||
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput();
|
||||
pvsInput.setPossibleValueSourceName(possibleValueSourceName);
|
||||
|
||||
if(dropdownData.getForeignKeyFieldName() != null)
|
||||
{
|
||||
////////////////////////////////////////
|
||||
// look for an id in the query params //
|
||||
////////////////////////////////////////
|
||||
Integer id = null;
|
||||
if(input.getQueryParams() != null && input.getQueryParams().containsKey("id") && StringUtils.hasContent(input.getQueryParams().get("id")))
|
||||
{
|
||||
if(!input.getQueryParams().containsKey(name) || !StringUtils.hasContent(input.getQueryParams().get(name)))
|
||||
{
|
||||
missingRequiredSelections.add(dropdownData.getLabel());
|
||||
}
|
||||
id = Integer.parseInt(input.getQueryParams().get("id"));
|
||||
}
|
||||
if(id != null)
|
||||
{
|
||||
pvsInput.setDefaultQueryFilter(new QQueryFilter().withCriteria(
|
||||
new QFilterCriteria(
|
||||
dropdownData.getForeignKeyFieldName(),
|
||||
QCriteriaOperator.EQUALS,
|
||||
id)));
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput);
|
||||
|
||||
List<Map<String, String>> dropdownOptionList = new ArrayList<>();
|
||||
pvsData.add(dropdownOptionList);
|
||||
|
||||
//////////////////////////////////////////
|
||||
// sort results, dedupe, and add to map //
|
||||
//////////////////////////////////////////
|
||||
Set<String> exists = new HashSet<>();
|
||||
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
|
||||
for(QPossibleValue<?> possibleValue : output.getResults())
|
||||
{
|
||||
String possibleValueSourceName = dropdownData.getPossibleValueSourceName();
|
||||
if(possibleValueSourceName != null)
|
||||
dropdownOptionList.add(MapBuilder.of(
|
||||
"id", String.valueOf(possibleValue.getId()),
|
||||
"label", possibleValue.getLabel()
|
||||
));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// because we know the dropdowns and what the field names will be when something is selected, we can make //
|
||||
// sure that something has been selected, and if not, display a message that a selection needs made //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(dropdownData.getIsRequired())
|
||||
{
|
||||
if(!input.getQueryParams().containsKey(possibleValueSourceName) || !StringUtils.hasContent(input.getQueryParams().get(possibleValueSourceName)))
|
||||
{
|
||||
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this looks complicated, but is just look for a label in the dropdown data and if found use it, //
|
||||
// otherwise look for label in PVS and if found use that, otherwise just use the PVS name //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String pvsLabel = dropdownData.getLabel() != null ? dropdownData.getLabel() : (possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName);
|
||||
labelList.add(pvsLabel);
|
||||
nameList.add(possibleValueSourceName);
|
||||
|
||||
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput();
|
||||
pvsInput.setPossibleValueSourceName(possibleValueSourceName);
|
||||
|
||||
if(dropdownData.getForeignKeyFieldName() != null)
|
||||
{
|
||||
////////////////////////////////////////
|
||||
// look for an id in the query params //
|
||||
////////////////////////////////////////
|
||||
Integer id = null;
|
||||
if(input.getQueryParams() != null && input.getQueryParams().containsKey("id") && StringUtils.hasContent(input.getQueryParams().get("id")))
|
||||
{
|
||||
id = Integer.parseInt(input.getQueryParams().get("id"));
|
||||
}
|
||||
if(id != null)
|
||||
{
|
||||
pvsInput.setDefaultQueryFilter(new QQueryFilter().withCriteria(
|
||||
new QFilterCriteria(
|
||||
dropdownData.getForeignKeyFieldName(),
|
||||
QCriteriaOperator.EQUALS,
|
||||
id)));
|
||||
}
|
||||
}
|
||||
|
||||
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput);
|
||||
|
||||
List<Map<String, String>> dropdownOptionList = new ArrayList<>();
|
||||
dataList.add(dropdownOptionList);
|
||||
|
||||
//////////////////////////////////////////
|
||||
// sort results, dedupe, and add to map //
|
||||
//////////////////////////////////////////
|
||||
Set<String> exists = new HashSet<>();
|
||||
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
|
||||
for(QPossibleValue<?> possibleValue : output.getResults())
|
||||
{
|
||||
dropdownOptionList.add(MapBuilder.of(
|
||||
"id", String.valueOf(possibleValue.getId()),
|
||||
"label", possibleValue.getLabel()
|
||||
));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// because we know the dropdowns and what the field names will be when something is selected, we can make //
|
||||
// sure that something has been selected, and if not, display a message that a selection needs made //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(dropdownData.getIsRequired())
|
||||
{
|
||||
if(!input.getQueryParams().containsKey(possibleValueSourceName) || !StringUtils.hasContent(input.getQueryParams().get(possibleValueSourceName)))
|
||||
{
|
||||
missingRequiredSelections.add(pvsLabel);
|
||||
}
|
||||
}
|
||||
missingRequiredSelections.add(pvsLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widgetData.setDropdownNameList(nameList);
|
||||
widgetData.setDropdownLabelList(labelList);
|
||||
widgetData.setDropdownDataList(dataList);
|
||||
widgetData.setDropdownNameList(pvsNames);
|
||||
widgetData.setDropdownLabelList(pvsLabels);
|
||||
widgetData.setDropdownDataList(pvsData);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// if there are any missing required dropdowns, build up a message to display //
|
||||
|
@ -36,7 +36,6 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
@ -60,7 +59,6 @@ import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -68,9 +66,6 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
*******************************************************************************/
|
||||
public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ChildRecordListRenderer.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -177,134 +172,126 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
try
|
||||
String widgetLabel = input.getQueryParams().get("widgetLabel");
|
||||
String joinName = input.getQueryParams().get("joinName");
|
||||
QJoinMetaData join = input.getInstance().getJoin(joinName);
|
||||
String id = input.getQueryParams().get("id");
|
||||
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
|
||||
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
|
||||
|
||||
Integer maxRows = null;
|
||||
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
|
||||
{
|
||||
String widgetLabel = input.getQueryParams().get("widgetLabel");
|
||||
String joinName = input.getQueryParams().get("joinName");
|
||||
QJoinMetaData join = input.getInstance().getJoin(joinName);
|
||||
String id = input.getQueryParams().get("id");
|
||||
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
|
||||
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
|
||||
}
|
||||
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
|
||||
{
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
|
||||
}
|
||||
|
||||
Integer maxRows = null;
|
||||
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// fetch the record that we're getting children for. e.g., the left-side of the join, with the input id //
|
||||
// but - only try this if we were given an id. note, this widget could be called for on an INSERT screen, where we don't have a record yet //
|
||||
// but we still want to be able to return all the other data in here that otherwise comes from the widget meta data, join, etc. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
int totalRows = 0;
|
||||
QRecord primaryRecord = null;
|
||||
QQueryFilter filter = null;
|
||||
QueryOutput queryOutput = new QueryOutput(new QueryInput());
|
||||
if(StringUtils.hasContent(id))
|
||||
{
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(join.getLeftTable());
|
||||
getInput.setPrimaryKey(id);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
primaryRecord = getOutput.getRecord();
|
||||
|
||||
if(primaryRecord == null)
|
||||
{
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
|
||||
}
|
||||
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
|
||||
{
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
|
||||
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// fetch the record that we're getting children for. e.g., the left-side of the join, with the input id //
|
||||
// but - only try this if we were given an id. note, this widget could be called for on an INSERT screen, where we don't have a record yet //
|
||||
// but we still want to be able to return all the other data in here that otherwise comes from the widget meta data, join, etc. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
int totalRows = 0;
|
||||
QRecord primaryRecord = null;
|
||||
QQueryFilter filter = null;
|
||||
QueryOutput queryOutput = new QueryOutput(new QueryInput());
|
||||
if(StringUtils.hasContent(id))
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// set up the query - for the table on the right side of the join //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
filter = new QQueryFilter();
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(join.getLeftTable());
|
||||
getInput.setPrimaryKey(id);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
primaryRecord = getOutput.getRecord();
|
||||
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(primaryRecord.getValue(joinOn.getLeftField()))));
|
||||
}
|
||||
filter.setOrderBys(join.getOrderBys());
|
||||
filter.setLimit(maxRows);
|
||||
|
||||
if(primaryRecord == null)
|
||||
{
|
||||
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
|
||||
}
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(join.getRightTable());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
queryInput.setFilter(filter);
|
||||
queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// set up the query - for the table on the right side of the join //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
filter = new QQueryFilter();
|
||||
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
|
||||
|
||||
totalRows = queryOutput.getRecords().size();
|
||||
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the input said to only do some max, and the # of results we got is that max, //
|
||||
// then do a count query, for displaying 1-n of <count> //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(join.getRightTable());
|
||||
countInput.setFilter(filter);
|
||||
totalRows = new CountAction().execute(countInput).getCount();
|
||||
}
|
||||
}
|
||||
|
||||
String tablePath = input.getInstance().getTablePath(rightTable.getName());
|
||||
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
|
||||
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
|
||||
|
||||
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
|
||||
{
|
||||
widgetData.setCanAddChildRecord(true);
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// new child records must have values from the join-ons //
|
||||
//////////////////////////////////////////////////////////
|
||||
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
|
||||
if(primaryRecord != null)
|
||||
{
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(primaryRecord.getValue(joinOn.getLeftField()))));
|
||||
}
|
||||
filter.setOrderBys(join.getOrderBys());
|
||||
filter.setLimit(maxRows);
|
||||
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(join.getRightTable());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
queryInput.setFilter(filter);
|
||||
queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
|
||||
|
||||
totalRows = queryOutput.getRecords().size();
|
||||
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the input said to only do some max, and the # of results we got is that max, //
|
||||
// then do a count query, for displaying 1-n of <count> //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(join.getRightTable());
|
||||
countInput.setFilter(filter);
|
||||
totalRows = new CountAction().execute(countInput).getCount();
|
||||
defaultValuesForNewChildRecords.put(joinOn.getRightField(), primaryRecord.getValue(joinOn.getLeftField()));
|
||||
}
|
||||
}
|
||||
|
||||
String tablePath = input.getInstance().getTablePath(rightTable.getName());
|
||||
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
|
||||
|
||||
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
|
||||
|
||||
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
|
||||
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
|
||||
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
|
||||
{
|
||||
widgetData.setCanAddChildRecord(true);
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// new child records must have values from the join-ons //
|
||||
//////////////////////////////////////////////////////////
|
||||
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
|
||||
if(primaryRecord != null)
|
||||
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there are no disabled fields specified - then normally any fields w/ a default value get implicitly disabled //
|
||||
// but - if we didn't look-up the primary record, then we'll want to explicit disable fields from joins //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(primaryRecord == null)
|
||||
{
|
||||
Set<String> implicitlyDisabledFields = new HashSet<>();
|
||||
widgetData.setDisabledFieldsForNewChildRecords(implicitlyDisabledFields);
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
defaultValuesForNewChildRecords.put(joinOn.getRightField(), primaryRecord.getValue(joinOn.getLeftField()));
|
||||
}
|
||||
}
|
||||
|
||||
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
|
||||
|
||||
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
|
||||
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
|
||||
{
|
||||
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there are no disabled fields specified - then normally any fields w/ a default value get implicitly disabled //
|
||||
// but - if we didn't look-up the primary record, then we'll want to explicit disable fields from joins //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(primaryRecord == null)
|
||||
{
|
||||
Set<String> implicitlyDisabledFields = new HashSet<>();
|
||||
widgetData.setDisabledFieldsForNewChildRecords(implicitlyDisabledFields);
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
implicitlyDisabledFields.add(joinOn.getRightField());
|
||||
}
|
||||
implicitlyDisabledFields.add(joinOn.getRightField());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (new RenderWidgetOutput(widgetData));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error rendering child record list", e, logPair("widgetName", () -> input.getWidgetMetaData().getName()));
|
||||
throw (e);
|
||||
}
|
||||
return (new RenderWidgetOutput(widgetData));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,69 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.interfaces;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface for actions that a backend can perform, based on streaming data
|
||||
** into the backend's storage.
|
||||
*******************************************************************************/
|
||||
public interface QStorageInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
OutputStream createOutputStream(StorageInput storageInput) throws QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
InputStream getInputStream(StorageInput storageInput) throws QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void makePublic(StorageInput storageInput) throws QException
|
||||
{
|
||||
//////////
|
||||
// noop //
|
||||
//////////
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default String getDownloadURL(StorageInput storageInput) throws QException
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.messaging;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.messaging.SendMessageInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.messaging.SendMessageOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.messaging.QMessagingProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.messaging.MessagingProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.modules.messaging.QMessagingProviderDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class SendMessageAction extends AbstractQActionFunction<SendMessageInput, SendMessageOutput>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public SendMessageOutput execute(SendMessageInput input) throws QException
|
||||
{
|
||||
if(!StringUtils.hasContent(input.getMessagingProviderName()))
|
||||
{
|
||||
throw (new QException("Messaging provider name was not given in SendMessageInput."));
|
||||
}
|
||||
|
||||
QMessagingProviderMetaData messagingProvider = QContext.getQInstance().getMessagingProvider(input.getMessagingProviderName());
|
||||
if(messagingProvider == null)
|
||||
{
|
||||
throw (new QException("Messaging provider named [" + input.getMessagingProviderName() + "] was not found in this QInstance."));
|
||||
}
|
||||
|
||||
MessagingProviderInterface messagingProviderInterface = new QMessagingProviderDispatcher().getMessagingProviderInterface(messagingProvider.getType());
|
||||
|
||||
return (messagingProviderInterface.sendMessage(input));
|
||||
}
|
||||
|
||||
}
|
@ -29,7 +29,6 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -42,18 +41,8 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
*******************************************************************************/
|
||||
public class JoinGraph
|
||||
{
|
||||
private Set<Edge> edges = new HashSet<>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// as an instance grows, with the number of joins (say, more than 50?), especially as they may have a lot of connections, //
|
||||
// it can become very very slow to process a full join graph (e.g., 10 seconds, maybe much worse, per Big-O...) //
|
||||
// also, it's not frequently useful to look at a join path that's more than a handful of tables long. //
|
||||
// thus - this property exists - to limit the max length of a join path. Keeping it small keeps instance enrichment //
|
||||
// and validation reasonably performant, at the possible cost of, some join-path that's longer than this limit may not //
|
||||
// be found - but - chances are, you don't want some 12-element join path to be used anyway, thus, this makes sense. //
|
||||
// but - it can be adjusted, per system property or ENV var. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private int maxPathLength = new QMetaDataVariableInterpreter().getIntegerFromPropertyOrEnvironment("qqq.instance.joinGraph.maxPathLength", "QQQ_INSTANCE_JOIN_GRAPH_MAX_PATH_LENGTH", 3);
|
||||
private Set<Edge> edges = new HashSet<>();
|
||||
|
||||
|
||||
|
||||
@ -314,13 +303,6 @@ public class JoinGraph
|
||||
|
||||
if(otherTableName != null)
|
||||
{
|
||||
if(newPath.size() > maxPathLength)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// performance hack. see comment at maxPathLength definition //
|
||||
////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
JoinConnectionList newConnectionList = connectionList.copy();
|
||||
JoinConnection joinConnection = new JoinConnection(otherTableName, edge.joinName);
|
||||
|
@ -23,19 +23,11 @@ package com.kingsrook.qqq.backend.core.actions.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -73,56 +65,4 @@ public class QProcessCallbackFactory
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QProcessCallback forRecordEntity(QRecordEntity entity)
|
||||
{
|
||||
return forRecord(entity.toQRecord());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QProcessCallback forRecord(QRecord record)
|
||||
{
|
||||
String primaryKeyField = "id";
|
||||
if(StringUtils.hasContent(record.getTableName()))
|
||||
{
|
||||
primaryKeyField = QContext.getQInstance().getTable(record.getTableName()).getPrimaryKeyField();
|
||||
}
|
||||
|
||||
Serializable primaryKeyValue = record.getValue(primaryKeyField);
|
||||
if(primaryKeyValue == null)
|
||||
{
|
||||
throw (new QRuntimeException("Record did not have value in its primary key field [" + primaryKeyField + "]"));
|
||||
}
|
||||
|
||||
return (forPrimaryKey(primaryKeyField, primaryKeyValue));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QProcessCallback forPrimaryKey(String fieldName, Serializable value)
|
||||
{
|
||||
return (forFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, value))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QProcessCallback forPrimaryKeys(String fieldName, Collection<? extends Serializable> values)
|
||||
{
|
||||
return (forFilter(new QQueryFilter().withCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.IN, values))));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -190,25 +190,7 @@ public class RunProcessAction
|
||||
// Run backend steps //
|
||||
///////////////////////
|
||||
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
|
||||
RunBackendStepOutput runBackendStepOutput = runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the step returned an override lastStepName, use that to determine how we proceed //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(runBackendStepOutput.getOverrideLastStepName() != null)
|
||||
{
|
||||
LOG.debug("Process step [" + lastStepName + "] returned an overrideLastStepName [" + runBackendStepOutput.getOverrideLastStepName() + "]!");
|
||||
lastStepName = runBackendStepOutput.getOverrideLastStepName();
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// similarly, if the step produced an updatedFrontendStepList, propagate that data outward //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(runBackendStepOutput.getUpdatedFrontendStepList() != null)
|
||||
{
|
||||
LOG.debug("Process step [" + lastStepName + "] generated an updatedFrontendStepList [" + runBackendStepOutput.getUpdatedFrontendStepList().stream().map(s -> s.getName()).toList() + "]!");
|
||||
runProcessOutput.setUpdatedFrontendStepList(runBackendStepOutput.getUpdatedFrontendStepList());
|
||||
}
|
||||
runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -357,7 +339,7 @@ public class RunProcessAction
|
||||
/*******************************************************************************
|
||||
** Run a single backend step.
|
||||
*******************************************************************************/
|
||||
private RunBackendStepOutput runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
|
||||
private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
|
||||
{
|
||||
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
|
||||
runBackendStepInput.setProcessName(process.getName());
|
||||
@ -386,16 +368,14 @@ public class RunProcessAction
|
||||
runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
|
||||
}
|
||||
|
||||
RunBackendStepOutput runBackendStepOutput = new RunBackendStepAction().execute(runBackendStepInput);
|
||||
storeState(stateKey, runBackendStepOutput.getProcessState());
|
||||
RunBackendStepOutput lastFunctionResult = new RunBackendStepAction().execute(runBackendStepInput);
|
||||
storeState(stateKey, lastFunctionResult.getProcessState());
|
||||
|
||||
if(runBackendStepOutput.getException() != null)
|
||||
if(lastFunctionResult.getException() != null)
|
||||
{
|
||||
runProcessOutput.setException(runBackendStepOutput.getException());
|
||||
throw (runBackendStepOutput.getException());
|
||||
runProcessOutput.setException(lastFunctionResult.getException());
|
||||
throw (lastFunctionResult.getException());
|
||||
}
|
||||
|
||||
return (runBackendStepOutput);
|
||||
}
|
||||
|
||||
|
||||
@ -515,15 +495,15 @@ public class RunProcessAction
|
||||
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if process specifies that it uses variants, look for that data in the session and append to our basepull key //
|
||||
// if backend specifies that it uses variants, look for that data in the session and append to our basepull key //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(process.getVariantBackend() != null)
|
||||
if(process.getSchedule() != null && process.getVariantBackend() != null)
|
||||
{
|
||||
QSession session = QContext.getQSession();
|
||||
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getVariantBackend());
|
||||
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
|
||||
{
|
||||
LOG.warn("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
|
||||
LOG.info("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -31,7 +31,6 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
@ -66,12 +65,12 @@ public class CsvExportStreamer implements ExportStreamerInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
|
||||
{
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
table = exportInput.getTable();
|
||||
outputStream = this.exportInput.getReportDestination().getReportOutputStream();
|
||||
outputStream = this.exportInput.getReportOutputStream();
|
||||
|
||||
writeTitleAndHeader();
|
||||
}
|
||||
|
@ -1,146 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Subclass of record pipe that ony allows through distinct records, based on
|
||||
** the set of fields specified in the constructor as a uniqueKey.
|
||||
*******************************************************************************/
|
||||
public class DistinctFilteringRecordPipe extends RecordPipe
|
||||
{
|
||||
private UniqueKey uniqueKey;
|
||||
private Set<Serializable> seenValues = new HashSet<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public DistinctFilteringRecordPipe(UniqueKey uniqueKey)
|
||||
{
|
||||
this.uniqueKey = uniqueKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that accepts pipe's overrideCapacity (allowed to be null)
|
||||
**
|
||||
*******************************************************************************/
|
||||
public DistinctFilteringRecordPipe(UniqueKey uniqueKey, Integer overrideCapacity)
|
||||
{
|
||||
super(overrideCapacity);
|
||||
this.uniqueKey = uniqueKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void addRecords(List<QRecord> records) throws QException
|
||||
{
|
||||
List<QRecord> recordsToAdd = new ArrayList<>();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(!seenBefore(record))
|
||||
{
|
||||
recordsToAdd.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
if(recordsToAdd.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
super.addRecords(recordsToAdd);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void addRecord(QRecord record) throws QException
|
||||
{
|
||||
if(seenBefore(record))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
super.addRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** return true if we've seen this record before (based on the unique key) -
|
||||
** also - update the set of seen values!
|
||||
*******************************************************************************/
|
||||
private boolean seenBefore(QRecord record)
|
||||
{
|
||||
Serializable ukValues = extractUKValues(record);
|
||||
if(seenValues.contains(ukValues))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
seenValues.add(ukValues);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Serializable extractUKValues(QRecord record)
|
||||
{
|
||||
if(uniqueKey.getFieldNames().size() == 1)
|
||||
{
|
||||
return (record.getValue(uniqueKey.getFieldNames().get(0)));
|
||||
}
|
||||
else
|
||||
{
|
||||
ArrayList<Serializable> rs = new ArrayList<>();
|
||||
for(String fieldName : uniqueKey.getFieldNames())
|
||||
{
|
||||
rs.add(record.getValue(fieldName));
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||
|
||||
|
||||
import java.io.OutputStream;
|
||||
@ -34,8 +34,8 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.ExcelStylerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
@ -43,9 +43,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.dhatim.fastexcel.StyleSetter;
|
||||
@ -54,19 +52,19 @@ import org.dhatim.fastexcel.Worksheet;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Excel export format implementation - built using fastexcel library
|
||||
** Excel export format implementation
|
||||
*******************************************************************************/
|
||||
public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
|
||||
public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ExcelFastexcelExportStreamer.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(ExcelExportStreamer.class);
|
||||
|
||||
private ExportInput exportInput;
|
||||
private QTableMetaData table;
|
||||
private List<QFieldMetaData> fields;
|
||||
private OutputStream outputStream;
|
||||
|
||||
private FastExcelStylerInterface fastExcelStylerInterface = new PlainFastExcelStyler();
|
||||
private Map<String, String> excelCellFormats;
|
||||
private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
|
||||
private Map<String, String> excelCellFormats;
|
||||
|
||||
private Workbook workbook;
|
||||
private Worksheet worksheet;
|
||||
@ -78,7 +76,7 @@ public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ExcelFastexcelExportStreamer()
|
||||
public ExcelExportStreamer()
|
||||
{
|
||||
}
|
||||
|
||||
@ -107,14 +105,14 @@ public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
|
||||
** Starts a new worksheet in the current workbook. Can be called multiple times.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
table = exportInput.getTable();
|
||||
outputStream = this.exportInput.getReportDestination().getReportOutputStream();
|
||||
outputStream = this.exportInput.getReportOutputStream();
|
||||
this.row = 0;
|
||||
this.sheetCount++;
|
||||
|
||||
@ -123,7 +121,7 @@ public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(workbook == null)
|
||||
{
|
||||
String appName = ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getBranding().getAppName(), "QQQ");
|
||||
String appName = "QQQ";
|
||||
QInstance instance = exportInput.getInstance();
|
||||
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
|
||||
{
|
||||
@ -169,7 +167,7 @@ public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
|
||||
worksheet.range(row, 0, row, fields.size() - 1).merge();
|
||||
|
||||
StyleSetter titleStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
|
||||
fastExcelStylerInterface.styleTitleRow(titleStyle);
|
||||
excelStylerInterface.styleTitleRow(titleStyle);
|
||||
titleStyle.set();
|
||||
|
||||
row++;
|
||||
@ -189,7 +187,7 @@ public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
|
||||
}
|
||||
|
||||
StyleSetter headerStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
|
||||
fastExcelStylerInterface.styleHeaderRow(headerStyle);
|
||||
excelStylerInterface.styleHeaderRow(headerStyle);
|
||||
headerStyle.set();
|
||||
|
||||
row++;
|
||||
@ -317,7 +315,7 @@ public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
|
||||
writeRecord(record);
|
||||
|
||||
StyleSetter totalsRowStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
|
||||
fastExcelStylerInterface.styleTotalsRow(totalsRowStyle);
|
||||
excelStylerInterface.styleTotalsRow(totalsRowStyle);
|
||||
totalsRowStyle.set();
|
||||
}
|
||||
|
@ -52,7 +52,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
@ -139,7 +138,7 @@ public class ExportAction
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check if this report format has a max-rows limit -- if so, do a count to verify we're under the limit //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ReportFormat reportFormat = exportInput.getReportDestination().getReportFormat();
|
||||
ReportFormat reportFormat = exportInput.getReportFormat();
|
||||
verifyCountUnderMax(exportInput, backendModule, reportFormat);
|
||||
|
||||
preExecuteRan = true;
|
||||
@ -190,9 +189,6 @@ public class ExportAction
|
||||
Set<String> addedJoinNames = new HashSet<>();
|
||||
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure that any tables being selected from are included as (LEFT) joins in the query //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String fieldName : exportInput.getFieldNames())
|
||||
{
|
||||
if(fieldName.contains("."))
|
||||
@ -201,7 +197,27 @@ public class ExportAction
|
||||
String joinTableName = parts[0];
|
||||
if(!addedJoinNames.contains(joinTableName))
|
||||
{
|
||||
queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
|
||||
QueryJoin queryJoin = new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true);
|
||||
queryJoins.add(queryJoin);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// in at least some cases, we need to let the queryJoin know what join-meta-data to use... //
|
||||
// This code basically mirrors what QFMD is doing right now, so it's better - //
|
||||
// but shouldn't all of this just be in JoinsContext? it does some of this... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData table = exportInput.getTable();
|
||||
Optional<ExposedJoin> exposedJoinOptional = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> ej.getJoinTable().equals(joinTableName)).findFirst();
|
||||
if(exposedJoinOptional.isEmpty())
|
||||
{
|
||||
throw (new QException("Could not find exposed join between base table " + table.getName() + " and requested join table " + joinTableName));
|
||||
}
|
||||
ExposedJoin exposedJoin = exposedJoinOptional.get();
|
||||
|
||||
if(exposedJoin.getJoinPath().size() == 1)
|
||||
{
|
||||
queryJoin.setJoinMetaData(QContext.getQInstance().getJoin(exposedJoin.getJoinPath().get(exposedJoin.getJoinPath().size() - 1)));
|
||||
}
|
||||
|
||||
addedJoinNames.add(joinTableName);
|
||||
}
|
||||
}
|
||||
@ -216,7 +232,6 @@ public class ExportAction
|
||||
}
|
||||
queryInput.getFilter().setLimit(exportInput.getLimit());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// tell this query that it needs to put its output into a pipe //
|
||||
@ -227,19 +242,10 @@ public class ExportAction
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set up a report streamer, which will read rows from the pipe, and write formatted report rows to the output stream //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ReportFormat reportFormat = exportInput.getReportDestination().getReportFormat();
|
||||
ReportFormat reportFormat = exportInput.getReportFormat();
|
||||
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
|
||||
List<QFieldMetaData> fields = getFields(exportInput);
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// it seems we can pass a view with just a name in here //
|
||||
//////////////////////////////////////////////////////////
|
||||
List<QReportView> views = new ArrayList<>();
|
||||
views.add(new QReportView()
|
||||
.withName("export"));
|
||||
|
||||
reportStreamer.preRun(exportInput.getReportDestination(), views);
|
||||
reportStreamer.start(exportInput, fields, "Sheet 1", views.get(0));
|
||||
reportStreamer.start(exportInput, fields, "Sheet 1");
|
||||
|
||||
//////////////////////////////////////////
|
||||
// run the query action as an async job //
|
||||
@ -328,7 +334,7 @@ public class ExportAction
|
||||
|
||||
try
|
||||
{
|
||||
exportInput.getReportDestination().getReportOutputStream().close();
|
||||
exportInput.getReportOutputStream().close();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
|
@ -26,10 +26,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -37,14 +35,20 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
*******************************************************************************/
|
||||
public interface ExportStreamerInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Called once, before any rows are available. Meant to write a header, for example.
|
||||
*******************************************************************************/
|
||||
void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException;
|
||||
|
||||
/*******************************************************************************
|
||||
** Called once, before any sheets are actually being produced.
|
||||
** Called as records flow into the pipe.
|
||||
******************************************************************************/
|
||||
void addRecords(List<QRecord> recordList) throws QReportingException;
|
||||
|
||||
/*******************************************************************************
|
||||
** Called once, after all rows are available. Meant to write a footer, or close resources, for example.
|
||||
*******************************************************************************/
|
||||
default void preRun(ReportDestination reportDestination, List<QReportView> views) throws QReportingException
|
||||
{
|
||||
// noop in base class
|
||||
}
|
||||
void finish() throws QReportingException;
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -54,20 +58,6 @@ public interface ExportStreamerInterface
|
||||
// noop in base class
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Called once per sheet, before any rows are available. Meant to write a
|
||||
** header, for example.
|
||||
**
|
||||
** If multiple sheets are being created, there is no separate end-sheet call.
|
||||
** Rather, a new one will just get started...
|
||||
*******************************************************************************/
|
||||
void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException;
|
||||
|
||||
/*******************************************************************************
|
||||
** Called as records flow into the pipe.
|
||||
******************************************************************************/
|
||||
void addRecords(List<QRecord> recordList) throws QReportingException;
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -75,11 +65,4 @@ public interface ExportStreamerInterface
|
||||
{
|
||||
addRecords(List.of(record));
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Called after all sheets are complete. Meant to do a final write, or close
|
||||
** resources, for example.
|
||||
*******************************************************************************/
|
||||
void finish() throws QReportingException;
|
||||
|
||||
}
|
||||
|
@ -24,8 +24,6 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@ -34,23 +32,18 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.DataSourceQueryInputCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
@ -58,9 +51,6 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutp
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
@ -78,17 +68,12 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.BackendStepPostRunInput;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.BackendStepPostRunOutput;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.Pair;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates;
|
||||
import com.kingsrook.qqq.backend.core.utils.aggregates.InstantAggregates;
|
||||
import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
|
||||
import com.kingsrook.qqq.backend.core.utils.aggregates.LocalDateAggregates;
|
||||
import com.kingsrook.qqq.backend.core.utils.aggregates.LongAggregates;
|
||||
import com.kingsrook.qqq.backend.core.utils.aggregates.StringAggregates;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -103,7 +88,7 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
** - summaries (like pivot tables, but called summary to avoid confusion with "native" pivot tables),
|
||||
** - native pivot tables (not initially supported, due to lack of support in fastexcel...).
|
||||
*******************************************************************************/
|
||||
public class GenerateReportAction extends AbstractQActionFunction<ReportInput, ReportOutput>
|
||||
public class GenerateReportAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(GenerateReportAction.class);
|
||||
|
||||
@ -117,67 +102,50 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
// Aggregates: (count:47;sum:10,000;max:2,000;min:15) //
|
||||
// salesSummaryReport > [(state:MO),(city:St.Louis)] > salePrice > (count:47;sum:10,000;max:2,000;min:15) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> summaryAggregates = new HashMap<>();
|
||||
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> varianceAggregates = new HashMap<>();
|
||||
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> summaryAggregates = new HashMap<>();
|
||||
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> varianceAggregates = new HashMap<>();
|
||||
|
||||
Map<String, AggregatesInterface<?, ?>> totalAggregates = new HashMap<>();
|
||||
Map<String, AggregatesInterface<?, ?>> varianceTotalAggregates = new HashMap<>();
|
||||
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>();
|
||||
Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>();
|
||||
|
||||
private QReportMetaData report;
|
||||
private ReportFormat reportFormat;
|
||||
private ExportStreamerInterface reportStreamer;
|
||||
private List<QReportDataSource> dataSources;
|
||||
private List<QReportView> views;
|
||||
|
||||
private Map<String, Integer> countByDataSource = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ReportOutput execute(ReportInput reportInput) throws QException
|
||||
public void execute(ReportInput reportInput) throws QException
|
||||
{
|
||||
ReportOutput reportOutput = new ReportOutput();
|
||||
QReportMetaData report = getReportMetaData(reportInput);
|
||||
|
||||
this.views = report.getViews();
|
||||
this.dataSources = report.getDataSources();
|
||||
|
||||
ReportFormat reportFormat = reportInput.getReportDestination().getReportFormat();
|
||||
report = reportInput.getInstance().getReport(reportInput.getReportName());
|
||||
reportFormat = reportInput.getReportFormat();
|
||||
if(reportFormat == null)
|
||||
{
|
||||
throw new QException("Report format was not specified.");
|
||||
}
|
||||
|
||||
if(reportInput.getOverrideExportStreamerSupplier() != null)
|
||||
{
|
||||
reportStreamer = reportInput.getOverrideExportStreamerSupplier().get();
|
||||
}
|
||||
else
|
||||
{
|
||||
reportStreamer = reportFormat.newReportStreamer();
|
||||
}
|
||||
|
||||
reportStreamer.preRun(reportInput.getReportDestination(), views);
|
||||
reportStreamer = reportFormat.newReportStreamer();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach data source, do a query (possibly more than 1, if it goes to multiple table views) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QReportDataSource dataSource : dataSources)
|
||||
for(QReportDataSource dataSource : report.getDataSources())
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// make a list of the views that use this data source for various purposes. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
List<QReportView> dataSourceTableViews = views.stream()
|
||||
List<QReportView> dataSourceTableViews = report.getViews().stream()
|
||||
.filter(v -> v.getType().equals(ReportType.TABLE))
|
||||
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
|
||||
.toList();
|
||||
|
||||
List<QReportView> dataSourceSummaryViews = views.stream()
|
||||
List<QReportView> dataSourceSummaryViews = report.getViews().stream()
|
||||
.filter(v -> v.getType().equals(ReportType.SUMMARY))
|
||||
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
|
||||
.toList();
|
||||
|
||||
List<QReportView> dataSourceVariantViews = views.stream()
|
||||
List<QReportView> dataSourceVariantViews = report.getViews().stream()
|
||||
.filter(v -> v.getType().equals(ReportType.SUMMARY))
|
||||
.filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName()))
|
||||
.toList();
|
||||
@ -216,50 +184,24 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// start the table-view (e.g., open this tab in xlsx) and then run the query-loop //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
startTableView(reportInput, dataSource, dataSourceTableView, reportFormat);
|
||||
startTableView(reportInput, dataSource, dataSourceTableView);
|
||||
gatherData(reportInput, dataSource, dataSourceTableView, dataSourceSummaryViews, dataSourceVariantViews);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// add pivot sheets //
|
||||
//////////////////////
|
||||
for(QReportView view : views)
|
||||
{
|
||||
if(view.getType().equals(ReportType.PIVOT))
|
||||
{
|
||||
if(reportFormat.getSupportsNativePivotTables())
|
||||
{
|
||||
startTableView(reportInput, null, view, reportFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Request to render a report with a PIVOT type view, for a format that does not support native pivot tables", logPair("reportFormat", reportFormat));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// there's no data to add to a pivot table, so nothing else to do here. //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
||||
|
||||
outputSummaries(reportInput);
|
||||
|
||||
reportOutput.setTotalRecordCount(countByDataSource.values().stream().mapToInt(Integer::intValue).sum());
|
||||
|
||||
reportStreamer.finish();
|
||||
|
||||
try
|
||||
{
|
||||
reportInput.getReportDestination().getReportOutputStream().close();
|
||||
reportInput.getReportOutputStream().close();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error completing report", e));
|
||||
}
|
||||
|
||||
return (reportOutput);
|
||||
}
|
||||
|
||||
|
||||
@ -267,48 +209,28 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QReportMetaData getReportMetaData(ReportInput reportInput) throws QException
|
||||
private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QException
|
||||
{
|
||||
if(reportInput.getReportMetaData() != null)
|
||||
{
|
||||
return reportInput.getReportMetaData();
|
||||
}
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
|
||||
if(StringUtils.hasContent(reportInput.getReportName()))
|
||||
{
|
||||
return QContext.getQInstance().getReport(reportInput.getReportName());
|
||||
}
|
||||
|
||||
throw (new QReportingException("ReportInput did not contain required parameters to identify the report being generated"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView, ReportFormat reportFormat) throws QException
|
||||
{
|
||||
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||
variableInterpreter.addValueMap("input", reportInput.getInputValues());
|
||||
|
||||
ExportInput exportInput = new ExportInput();
|
||||
exportInput.setReportDestination(reportInput.getReportDestination());
|
||||
exportInput.setReportFormat(reportFormat);
|
||||
exportInput.setFilename(reportInput.getFilename());
|
||||
exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
|
||||
exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow());
|
||||
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
||||
|
||||
JoinsContext joinsContext = null;
|
||||
if(dataSource != null)
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
|
||||
countDataSourceRecords(reportInput, dataSource, reportFormat);
|
||||
}
|
||||
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
|
||||
}
|
||||
|
||||
List<QFieldMetaData> fields = new ArrayList<>();
|
||||
for(QReportField column : CollectionUtils.nonNullList(reportView.getColumns()))
|
||||
for(QReportField column : reportView.getColumns())
|
||||
{
|
||||
if(column.getIsVirtual())
|
||||
{
|
||||
@ -320,7 +242,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext == null ? null : joinsContext.getFieldAndTableNameOrAlias(effectiveFieldName);
|
||||
if(fieldAndTableNameOrAlias == null || fieldAndTableNameOrAlias.field() == null)
|
||||
{
|
||||
throw new QReportingException("Could not find field named [" + effectiveFieldName + "] in dataSource [" + (dataSource == null ? null : dataSource.getName()) + "]");
|
||||
throw new QReportingException("Could not find field named [" + effectiveFieldName + "] in dataSource [" + dataSource.getName() + "]");
|
||||
}
|
||||
|
||||
QFieldMetaData field = fieldAndTableNameOrAlias.field().clone();
|
||||
@ -334,7 +256,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
}
|
||||
|
||||
reportStreamer.setDisplayFormats(getDisplayFormatMap(fields));
|
||||
reportStreamer.start(exportInput, fields, reportView.getLabel(), reportView);
|
||||
reportStreamer.start(exportInput, fields, reportView.getLabel());
|
||||
}
|
||||
|
||||
|
||||
@ -342,36 +264,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void countDataSourceRecords(ReportInput reportInput, QReportDataSource dataSource, ReportFormat reportFormat) throws QException
|
||||
{
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
|
||||
setInputValuesInQueryFilter(reportInput, queryFilter);
|
||||
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(dataSource.getSourceTable());
|
||||
countInput.setFilter(queryFilter);
|
||||
countInput.setQueryJoins(dataSource.getQueryJoins());
|
||||
CountOutput countOutput = new CountAction().execute(countInput);
|
||||
|
||||
if(countOutput.getCount() != null)
|
||||
{
|
||||
countByDataSource.put(dataSource.getName(), countOutput.getCount());
|
||||
|
||||
if(reportFormat.getMaxRows() != null && countOutput.getCount() > reportFormat.getMaxRows())
|
||||
{
|
||||
throw (new QUserFacingException("The requested report would include more rows ("
|
||||
+ String.format("%,d", countOutput.getCount()) + ") than the maximum allowed ("
|
||||
+ String.format("%,d", reportFormat.getMaxRows()) + ") for the selected file format (" + reportFormat + ")."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Integer gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
|
||||
private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check if this view has a transform step - if so, set it up now and run its pre-run //
|
||||
@ -398,9 +291,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
RunBackendStepInput finalTransformStepInput = transformStepInput;
|
||||
RunBackendStepOutput finalTransformStepOutput = transformStepOutput;
|
||||
|
||||
String tableLabel = ObjectUtils.tryElse(() -> QContext.getQInstance().getTable(dataSource.getSourceTable()).getLabel(), Objects.requireNonNullElse(dataSource.getSourceTable(), ""));
|
||||
AtomicInteger consumedCount = new AtomicInteger(0);
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// run a record pipe loop, over the query for this data source //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
@ -417,7 +307,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
queryInput.setTableName(dataSource.getSourceTable());
|
||||
queryInput.setFilter(queryFilter);
|
||||
queryInput.setQueryJoins(dataSource.getQueryJoins());
|
||||
queryInput.withQueryHint(QueryInput.QueryHint.POTENTIALLY_LARGE_NUMBER_OF_RESULTS);
|
||||
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryInput.getFilter())));
|
||||
@ -457,21 +346,10 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
if(finalTransformStep != null)
|
||||
{
|
||||
finalTransformStepInput.setRecords(records);
|
||||
finalTransformStep.runOnePage(finalTransformStepInput, finalTransformStepOutput);
|
||||
finalTransformStep.run(finalTransformStepInput, finalTransformStepOutput);
|
||||
records = finalTransformStepOutput.getRecords();
|
||||
}
|
||||
|
||||
Integer total = countByDataSource.get(dataSource.getName());
|
||||
if(total != null)
|
||||
{
|
||||
reportInput.getAsyncJobCallback().updateStatus("Processing " + tableLabel + " records", consumedCount.get() + 1, total);
|
||||
}
|
||||
else
|
||||
{
|
||||
reportInput.getAsyncJobCallback().updateStatus("Processing " + tableLabel + " records (" + String.format("%,d", consumedCount.get() + 1) + ")");
|
||||
}
|
||||
consumedCount.getAndAdd(records.size());
|
||||
|
||||
return (consumeRecords(reportInput, dataSource, records, tableView, summaryViews, variantViews));
|
||||
});
|
||||
|
||||
@ -482,8 +360,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
{
|
||||
transformStep.postRun(new BackendStepPostRunInput(transformStepInput), new BackendStepPostRunOutput(transformStepOutput));
|
||||
}
|
||||
|
||||
return consumedCount.get();
|
||||
}
|
||||
|
||||
|
||||
@ -491,11 +367,11 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext) throws QException
|
||||
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext)
|
||||
{
|
||||
Set<String> fieldsToTranslatePossibleValues = new HashSet<>();
|
||||
|
||||
for(QReportView view : views)
|
||||
for(QReportView view : report.getViews())
|
||||
{
|
||||
for(QReportField column : CollectionUtils.nonNullList(view.getColumns()))
|
||||
{
|
||||
@ -509,16 +385,15 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
}
|
||||
}
|
||||
|
||||
for(String summaryFieldName : CollectionUtils.nonNullList(view.getSummaryFields()))
|
||||
for(String summaryField : CollectionUtils.nonNullList(view.getPivotFields()))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// all pivotFields that are possible value sources are implicitly translated //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData mainTable = QContext.getQInstance().getTable(dataSource.getSourceTable());
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(mainTable, summaryFieldName);
|
||||
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
if(table.getField(summaryField).getPossibleValueSourceName() != null)
|
||||
{
|
||||
fieldsToTranslatePossibleValues.add(summaryFieldName);
|
||||
fieldsToTranslatePossibleValues.add(summaryField);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -531,33 +406,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static FieldAndJoinTable getFieldAndJoinTable(QTableMetaData mainTable, String fieldName) throws QException
|
||||
{
|
||||
if(fieldName.indexOf('.') > -1)
|
||||
{
|
||||
String joinTableName = fieldName.replaceAll("\\..*", "");
|
||||
String joinFieldName = fieldName.replaceAll(".*\\.", "");
|
||||
|
||||
QTableMetaData joinTable = QContext.getQInstance().getTable(joinTableName);
|
||||
if(joinTable == null)
|
||||
{
|
||||
throw (new QException("Unrecognized join table name: " + joinTableName));
|
||||
}
|
||||
|
||||
return new FieldAndJoinTable(joinTable.getField(joinFieldName), joinTable);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new FieldAndJoinTable(mainTable.getField(fieldName), mainTable);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter) throws QException
|
||||
private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter)
|
||||
{
|
||||
if(queryFilter == null || queryFilter.getCriteria() == null)
|
||||
{
|
||||
@ -646,27 +495,26 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> aggregatesMap) throws QException
|
||||
private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> aggregatesMap)
|
||||
{
|
||||
Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
|
||||
Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
SummaryKey key = new SummaryKey();
|
||||
for(String summaryFieldName : view.getSummaryFields())
|
||||
for(String summaryField : view.getPivotFields())
|
||||
{
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
|
||||
Serializable summaryValue = record.getValue(summaryFieldName);
|
||||
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
|
||||
Serializable summaryValue = record.getValue(summaryField);
|
||||
if(table.getField(summaryField).getPossibleValueSourceName() != null)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// so, this is kinda a thing - where we implicitly use possible-value labels (e.g., display values) for pivot fields... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
summaryValue = record.getDisplayValue(summaryFieldName);
|
||||
summaryValue = record.getDisplayValue(summaryField);
|
||||
}
|
||||
key.add(summaryFieldName, summaryValue);
|
||||
key.add(summaryField, summaryValue);
|
||||
|
||||
if(view.getIncludeSummarySubTotals() && key.getKeys().size() < view.getSummaryFields().size())
|
||||
if(view.getIncludePivotSubTotals() && key.getKeys().size() < view.getPivotFields().size())
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// be careful here, with these key objects, and their identity, being used as map keys //
|
||||
@ -685,9 +533,9 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates, SummaryKey key) throws QException
|
||||
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates, SummaryKey key)
|
||||
{
|
||||
Map<String, AggregatesInterface<?, ?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
|
||||
Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
|
||||
addRecordToAggregatesMap(table, record, keyAggregates);
|
||||
}
|
||||
|
||||
@ -696,74 +544,29 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?, ?>> aggregatesMap) throws QException
|
||||
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?>> aggregatesMap)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - an optimization could be, to only compute aggregates that we'll need... //
|
||||
// Only if we measure and see this to be slow - it may be, lots of BigDecimal math? //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String fieldName : record.getValues().keySet())
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
QFieldMetaData field = null;
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////////////
|
||||
// todo - memoize this, if we ever need to optimize //
|
||||
//////////////////////////////////////////////////////
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
|
||||
field = fieldAndJoinTable.field();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// for non-real-fields... let's skip for now - but maybe treat as string in future? //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.debug("Couldn't find field in table qInstance - won't compute aggregates for it", logPair("fieldName", fieldName));
|
||||
continue;
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
|
||||
if(field.getType().equals(QFieldType.INTEGER))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
AggregatesInterface<String, ?> fieldAggregates = (AggregatesInterface<String, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new StringAggregates());
|
||||
fieldAggregates.add(record.getDisplayValue(fieldName));
|
||||
}
|
||||
else if(field.getType().equals(QFieldType.INTEGER))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
AggregatesInterface<Integer, ?> fieldAggregates = (AggregatesInterface<Integer, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new IntegerAggregates());
|
||||
fieldAggregates.add(record.getValueInteger(fieldName));
|
||||
AggregatesInterface<Integer> fieldAggregates = (AggregatesInterface<Integer>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
|
||||
fieldAggregates.add(record.getValueInteger(field.getName()));
|
||||
}
|
||||
else if(field.getType().equals(QFieldType.LONG))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
AggregatesInterface<Long, ?> fieldAggregates = (AggregatesInterface<Long, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new LongAggregates());
|
||||
fieldAggregates.add(record.getValueLong(fieldName));
|
||||
AggregatesInterface<Long> fieldAggregates = (AggregatesInterface<Long>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new LongAggregates());
|
||||
fieldAggregates.add(record.getValueLong(field.getName()));
|
||||
}
|
||||
else if(field.getType().equals(QFieldType.DECIMAL))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
AggregatesInterface<BigDecimal, ?> fieldAggregates = (AggregatesInterface<BigDecimal, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new BigDecimalAggregates());
|
||||
fieldAggregates.add(record.getValueBigDecimal(fieldName));
|
||||
}
|
||||
else if(field.getType().equals(QFieldType.DATE_TIME))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
AggregatesInterface<Instant, ?> fieldAggregates = (AggregatesInterface<Instant, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new InstantAggregates());
|
||||
fieldAggregates.add(record.getValueInstant(fieldName));
|
||||
}
|
||||
else if(field.getType().equals(QFieldType.DATE))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
AggregatesInterface<LocalDate, ?> fieldAggregates = (AggregatesInterface<LocalDate, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new LocalDateAggregates());
|
||||
fieldAggregates.add(record.getValueLocalDate(fieldName));
|
||||
}
|
||||
if(field.getType().isStringLike())
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
AggregatesInterface<String, ?> fieldAggregates = (AggregatesInterface<String, ?>) aggregatesMap.computeIfAbsent(fieldName, (name) -> new StringAggregates());
|
||||
fieldAggregates.add(record.getValueString(fieldName));
|
||||
AggregatesInterface<BigDecimal> fieldAggregates = (AggregatesInterface<BigDecimal>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
|
||||
fieldAggregates.add(record.getValueBigDecimal(field.getName()));
|
||||
}
|
||||
// todo - more types (dates, at least?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -772,22 +575,24 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void outputSummaries(ReportInput reportInput) throws QException
|
||||
private void outputSummaries(ReportInput reportInput) throws QReportingException, QFormulaException
|
||||
{
|
||||
List<QReportView> reportViews = views.stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
|
||||
List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
|
||||
for(QReportView view : reportViews)
|
||||
{
|
||||
QReportDataSource dataSource = getDataSource(view.getDataSourceName());
|
||||
QReportDataSource dataSource = report.getDataSource(view.getDataSourceName());
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
|
||||
|
||||
ExportInput exportInput = new ExportInput();
|
||||
exportInput.setReportDestination(reportInput.getReportDestination());
|
||||
exportInput.setReportFormat(reportFormat);
|
||||
exportInput.setFilename(reportInput.getFilename());
|
||||
exportInput.setTitleRow(summaryOutput.titleRow);
|
||||
exportInput.setIncludeHeaderRow(view.getIncludeHeaderRow());
|
||||
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
||||
|
||||
reportStreamer.setDisplayFormats(getDisplayFormatMap(view));
|
||||
reportStreamer.start(exportInput, getFields(table, view), view.getLabel(), view);
|
||||
reportStreamer.start(exportInput, getFields(table, view), view.getLabel());
|
||||
|
||||
reportStreamer.addRecords(summaryOutput.summaryRows); // todo - what if this set is huge?
|
||||
|
||||
@ -800,24 +605,6 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QReportDataSource getDataSource(String dataSourceName)
|
||||
{
|
||||
for(QReportDataSource dataSource : CollectionUtils.nonNullList(dataSources))
|
||||
{
|
||||
if(dataSource.getName().equals(dataSourceName))
|
||||
{
|
||||
return (dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -845,13 +632,13 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QFieldMetaData> getFields(QTableMetaData table, QReportView view) throws QException
|
||||
private List<QFieldMetaData> getFields(QTableMetaData table, QReportView view)
|
||||
{
|
||||
List<QFieldMetaData> fields = new ArrayList<>();
|
||||
for(String summaryFieldName : view.getSummaryFields())
|
||||
for(String pivotField : view.getPivotFields())
|
||||
{
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
|
||||
fields.add(new QFieldMetaData(summaryFieldName, fieldAndJoinTable.field().getType()).withLabel(fieldAndJoinTable.field().getLabel())); // todo do we need the type? if so need table as input here
|
||||
QFieldMetaData field = table.getField(pivotField);
|
||||
fields.add(new QFieldMetaData(pivotField, field.getType()).withLabel(field.getLabel())); // todo do we need the type? if so need table as input here
|
||||
}
|
||||
for(QReportField column : view.getColumns())
|
||||
{
|
||||
@ -881,11 +668,11 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
// create summary rows //
|
||||
/////////////////////////
|
||||
List<QRecord> summaryRows = new ArrayList<>();
|
||||
for(Map.Entry<SummaryKey, Map<String, AggregatesInterface<?, ?>>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
|
||||
for(Map.Entry<SummaryKey, Map<String, AggregatesInterface<?>>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
|
||||
{
|
||||
SummaryKey summaryKey = entry.getKey();
|
||||
Map<String, AggregatesInterface<?, ?>> fieldAggregates = entry.getValue();
|
||||
Map<String, Serializable> summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
|
||||
SummaryKey summaryKey = entry.getKey();
|
||||
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
|
||||
Map<String, Serializable> summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
|
||||
variableInterpreter.addValueMap("pivot", summaryValues);
|
||||
variableInterpreter.addValueMap("summary", summaryValues);
|
||||
|
||||
@ -894,9 +681,9 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
|
||||
if(!varianceAggregates.isEmpty())
|
||||
{
|
||||
Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
|
||||
Map<String, AggregatesInterface<?, ?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
|
||||
Map<String, Serializable> varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
|
||||
Map<SummaryKey, Map<String, AggregatesInterface<?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
|
||||
Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
|
||||
Map<String, Serializable> varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
|
||||
variableInterpreter.addValueMap("variancePivot", varianceValues);
|
||||
variableInterpreter.addValueMap("variance", varianceValues);
|
||||
}
|
||||
@ -915,7 +702,7 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// for summary subtotals, add the text "Total" to the last field in this key //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
if(summaryKey.getKeys().size() < view.getSummaryFields().size())
|
||||
if(summaryKey.getKeys().size() < view.getPivotFields().size())
|
||||
{
|
||||
String fieldName = summaryKey.getKeys().get(summaryKey.getKeys().size() - 1).getA();
|
||||
summaryRow.setValue(fieldName, summaryRow.getValueString(fieldName) + " Total");
|
||||
@ -953,11 +740,11 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
{
|
||||
totalRow = new QRecord();
|
||||
|
||||
for(String summaryField : view.getSummaryFields())
|
||||
for(String pivotField : view.getPivotFields())
|
||||
{
|
||||
if(totalRow.getValues().isEmpty())
|
||||
{
|
||||
totalRow.setValue(summaryField, "Totals");
|
||||
totalRow.setValue(pivotField, "Totals");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1077,24 +864,18 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Map<String, Serializable> getSummaryValuesForInterpreter(Map<String, AggregatesInterface<?, ?>> fieldAggregates)
|
||||
private Map<String, Serializable> getSummaryValuesForInterpreter(Map<String, AggregatesInterface<?>> fieldAggregates)
|
||||
{
|
||||
Map<String, Serializable> summaryValuesForInterpreter = new HashMap<>();
|
||||
for(Map.Entry<String, AggregatesInterface<?, ?>> subEntry : fieldAggregates.entrySet())
|
||||
for(Map.Entry<String, AggregatesInterface<?>> subEntry : fieldAggregates.entrySet())
|
||||
{
|
||||
String fieldName = subEntry.getKey();
|
||||
AggregatesInterface<?, ?> aggregates = subEntry.getValue();
|
||||
String fieldName = subEntry.getKey();
|
||||
AggregatesInterface<?> aggregates = subEntry.getValue();
|
||||
summaryValuesForInterpreter.put("sum." + fieldName, aggregates.getSum());
|
||||
summaryValuesForInterpreter.put("count." + fieldName, aggregates.getCount());
|
||||
summaryValuesForInterpreter.put("count_nums." + fieldName, aggregates.getCount());
|
||||
summaryValuesForInterpreter.put("min." + fieldName, aggregates.getMin());
|
||||
summaryValuesForInterpreter.put("max." + fieldName, aggregates.getMax());
|
||||
summaryValuesForInterpreter.put("average." + fieldName, aggregates.getAverage());
|
||||
summaryValuesForInterpreter.put("product." + fieldName, aggregates.getProduct());
|
||||
summaryValuesForInterpreter.put("var." + fieldName, aggregates.getVariance());
|
||||
summaryValuesForInterpreter.put("varp." + fieldName, aggregates.getVarP());
|
||||
summaryValuesForInterpreter.put("std_dev." + fieldName, aggregates.getStandardDeviation());
|
||||
summaryValuesForInterpreter.put("std_devp." + fieldName, aggregates.getStdDevP());
|
||||
}
|
||||
return summaryValuesForInterpreter;
|
||||
}
|
||||
@ -1108,27 +889,4 @@ public class GenerateReportAction extends AbstractQActionFunction<ReportInput, R
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable)
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getLabel(QTableMetaData mainTable)
|
||||
{
|
||||
if(mainTable.getName().equals(joinTable.getName()))
|
||||
{
|
||||
return (field.getLabel());
|
||||
}
|
||||
else
|
||||
{
|
||||
return (joinTable.getLabel() + ": " + field.getLabel());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,23 +26,17 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -52,23 +46,13 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(JsonExportStreamer.class);
|
||||
|
||||
private boolean prettyPrint = true;
|
||||
|
||||
private ExportInput exportInput;
|
||||
private QTableMetaData table;
|
||||
private List<QFieldMetaData> fields;
|
||||
private OutputStream outputStream;
|
||||
|
||||
private boolean multipleViews = false;
|
||||
private boolean haveStartedAnyViews = false;
|
||||
|
||||
private boolean needCommaBeforeRecord = false;
|
||||
|
||||
private byte[] indent = new byte[0];
|
||||
private String indentString = "";
|
||||
|
||||
private Pattern colonLetterPattern = Pattern.compile(":([A-Z]+)($|[A-Z][a-z])");
|
||||
private Memoization<String, String> fieldLabelMemoization = new Memoization<>();
|
||||
private boolean needComma = false;
|
||||
private boolean prettyPrint = true;
|
||||
|
||||
|
||||
|
||||
@ -85,124 +69,21 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void preRun(ReportDestination reportDestination, List<QReportView> views) throws QReportingException
|
||||
{
|
||||
outputStream = reportDestination.getReportOutputStream();
|
||||
|
||||
if(views.size() > 1)
|
||||
{
|
||||
multipleViews = true;
|
||||
}
|
||||
|
||||
if(multipleViews)
|
||||
{
|
||||
try
|
||||
{
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write('[');
|
||||
newlineIfPretty(outputStream);
|
||||
increaseIndent();
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw (new QReportingException("Error starting report output", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
|
||||
{
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
table = exportInput.getTable();
|
||||
|
||||
needCommaBeforeRecord = false;
|
||||
outputStream = this.exportInput.getReportOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
if(multipleViews)
|
||||
{
|
||||
if(haveStartedAnyViews)
|
||||
{
|
||||
/////////////////////////
|
||||
// close the last view //
|
||||
/////////////////////////
|
||||
newlineIfPretty(outputStream);
|
||||
|
||||
decreaseIndent();
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write(']');
|
||||
newlineIfPretty(outputStream);
|
||||
|
||||
decreaseIndent();
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write('}');
|
||||
outputStream.write(',');
|
||||
newlineIfPretty(outputStream);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// open a new view, as an object, with a name & data entry //
|
||||
/////////////////////////////////////////////////////////////
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write('{');
|
||||
newlineIfPretty(outputStream);
|
||||
increaseIndent();
|
||||
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write(String.format("""
|
||||
"name":"%s",""", label).getBytes(StandardCharsets.UTF_8));
|
||||
newlineIfPretty(outputStream);
|
||||
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write("""
|
||||
"data":""".getBytes(StandardCharsets.UTF_8));
|
||||
newlineIfPretty(outputStream);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// start the array of entries for this view //
|
||||
//////////////////////////////////////////////
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write('[');
|
||||
increaseIndent();
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw (new QReportingException("Error starting report output", e));
|
||||
}
|
||||
|
||||
haveStartedAnyViews = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void increaseIndent()
|
||||
{
|
||||
indent = new byte[indent.length + 3];
|
||||
Arrays.fill(indent, (byte) ' ');
|
||||
indentString = new String(indent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void decreaseIndent()
|
||||
{
|
||||
indent = new byte[Math.max(0, indent.length - 3)];
|
||||
Arrays.fill(indent, (byte) ' ');
|
||||
indentString = new String(indent);
|
||||
}
|
||||
|
||||
|
||||
@ -230,7 +111,7 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
try
|
||||
{
|
||||
if(needCommaBeforeRecord)
|
||||
if(needComma)
|
||||
{
|
||||
outputStream.write(',');
|
||||
}
|
||||
@ -238,25 +119,21 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
Map<String, Serializable> mapForJson = new LinkedHashMap<>();
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
mapForJson.put(getLabelForJson(field), qRecord.getValue(field.getName()));
|
||||
String labelForJson = StringUtils.lcFirst(field.getLabel().replace(" ", ""));
|
||||
mapForJson.put(labelForJson, qRecord.getValue(field.getName()));
|
||||
}
|
||||
|
||||
String json = prettyPrint ? JsonUtils.toPrettyJson(mapForJson) : JsonUtils.toJson(mapForJson);
|
||||
if(prettyPrint)
|
||||
{
|
||||
json = json.replaceAll("(?s)\n", "\n" + indentString);
|
||||
}
|
||||
|
||||
if(prettyPrint)
|
||||
{
|
||||
outputStream.write('\n');
|
||||
}
|
||||
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
outputStream.flush(); // todo - less often?
|
||||
needCommaBeforeRecord = true;
|
||||
needComma = true;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -266,73 +143,6 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
String getLabelForJson(QFieldMetaData field)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// memoize, to avoid running these regex/replacements millions of times //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
Optional<String> result = fieldLabelMemoization.getResult(field.getName(), fieldName ->
|
||||
{
|
||||
String labelForJson = field.getLabel().replace(" ", "");
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// now fix up any field-name-parts after the table: portion of a join name //
|
||||
// lineItem:SKU to become lineItem:sku //
|
||||
// parcel:SLAStatus to become parcel:slaStatus //
|
||||
// order:Client to become order:client //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
Matcher allCaps = Pattern.compile("^[A-Z]+$").matcher(labelForJson);
|
||||
Matcher startsAllCapsThenNextWordMatcher = Pattern.compile("([A-Z]+)([A-Z][a-z].*)").matcher(labelForJson);
|
||||
Matcher startsOneCapMatcher = Pattern.compile("([A-Z])(.*)").matcher(labelForJson);
|
||||
|
||||
if(allCaps.matches())
|
||||
{
|
||||
labelForJson = allCaps.replaceAll(m -> m.group().toLowerCase());
|
||||
}
|
||||
else if(startsAllCapsThenNextWordMatcher.matches())
|
||||
{
|
||||
labelForJson = startsAllCapsThenNextWordMatcher.replaceAll(m -> m.group(1).toLowerCase() + m.group(2));
|
||||
}
|
||||
else if(startsOneCapMatcher.matches())
|
||||
{
|
||||
labelForJson = startsOneCapMatcher.replaceAll(m -> m.group(1).toLowerCase() + m.group(2));
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// now fix up any field-name-parts after the table: portion of a join name //
|
||||
// lineItem:SKU to become lineItem:sku //
|
||||
// parcel:SLAStatus to become parcel:slaStatus //
|
||||
// order:Client to become order:client //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
Matcher colonThenAllCapsThenEndMatcher = Pattern.compile("(.*:)([A-Z]+)$").matcher(labelForJson);
|
||||
Matcher colonThenAllCapsThenNextWordMatcher = Pattern.compile("(.*:)([A-Z]+)([A-Z][a-z].*)").matcher(labelForJson);
|
||||
Matcher colonThenOneCapMatcher = Pattern.compile("(.*:)([A-Z])(.*)").matcher(labelForJson);
|
||||
|
||||
if(colonThenAllCapsThenEndMatcher.matches())
|
||||
{
|
||||
labelForJson = colonThenAllCapsThenEndMatcher.replaceAll(m -> m.group(1) + m.group(2).toLowerCase());
|
||||
}
|
||||
else if(colonThenAllCapsThenNextWordMatcher.matches())
|
||||
{
|
||||
labelForJson = colonThenAllCapsThenNextWordMatcher.replaceAll(m -> m.group(1) + m.group(2).toLowerCase() + m.group(3));
|
||||
}
|
||||
else if(colonThenOneCapMatcher.matches())
|
||||
{
|
||||
labelForJson = colonThenOneCapMatcher.replaceAll(m -> m.group(1) + m.group(2).toLowerCase() + m.group(3));
|
||||
}
|
||||
|
||||
System.out.println("Label: " + labelForJson);
|
||||
return (labelForJson);
|
||||
});
|
||||
|
||||
return result.orElse(field.getName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -352,34 +162,11 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////
|
||||
// close the array of entries for this view //
|
||||
//////////////////////////////////////////////
|
||||
newlineIfPretty(outputStream);
|
||||
|
||||
decreaseIndent();
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write(']');
|
||||
newlineIfPretty(outputStream);
|
||||
|
||||
if(multipleViews)
|
||||
if(prettyPrint)
|
||||
{
|
||||
////////////////////////////////////////////
|
||||
// close this view, if there are multiple //
|
||||
////////////////////////////////////////////
|
||||
decreaseIndent();
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write('}');
|
||||
newlineIfPretty(outputStream);
|
||||
|
||||
/////////////////////////////
|
||||
// close the list of views //
|
||||
/////////////////////////////
|
||||
decreaseIndent();
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write(']');
|
||||
newlineIfPretty(outputStream);
|
||||
outputStream.write('\n');
|
||||
}
|
||||
outputStream.write(']');
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
@ -387,30 +174,4 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void newlineIfPretty(OutputStream outputStream) throws IOException
|
||||
{
|
||||
if(prettyPrint)
|
||||
{
|
||||
outputStream.write('\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void indentIfPretty(OutputStream outputStream) throws IOException
|
||||
{
|
||||
if(prettyPrint)
|
||||
{
|
||||
outputStream.write(indent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -88,7 +87,7 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
|
||||
{
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
|
@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -44,9 +43,8 @@ public class RecordPipe
|
||||
|
||||
private static final long BLOCKING_SLEEP_MILLIS = 100;
|
||||
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes
|
||||
private static final int DEFAULT_CAPACITY = 1_000;
|
||||
|
||||
private int capacity = DEFAULT_CAPACITY;
|
||||
private int capacity = 1_000;
|
||||
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(capacity);
|
||||
|
||||
private boolean isTerminated = false;
|
||||
@ -72,13 +70,11 @@ public class RecordPipe
|
||||
|
||||
/*******************************************************************************
|
||||
** Construct a record pipe, with an alternative capacity for the internal queue.
|
||||
**
|
||||
** overrideCapacity is allowed to be null - in which case, DEFAULT_CAPACITY is used.
|
||||
*******************************************************************************/
|
||||
public RecordPipe(Integer overrideCapacity)
|
||||
{
|
||||
this.capacity = Objects.requireNonNullElse(overrideCapacity, DEFAULT_CAPACITY);
|
||||
queue = new ArrayBlockingQueue<>(this.capacity);
|
||||
this.capacity = overrideCapacity;
|
||||
queue = new ArrayBlockingQueue<>(overrideCapacity);
|
||||
}
|
||||
|
||||
|
||||
@ -142,7 +138,7 @@ public class RecordPipe
|
||||
{
|
||||
if(now - sleepLoopStartTime > MAX_SLEEP_LOOP_MILLIS)
|
||||
{
|
||||
LOG.warn("Giving up adding record to pipe, due to pipe being full for more than " + MAX_SLEEP_LOOP_MILLIS + " millis");
|
||||
LOG.warn("Giving up adding record to pipe, due to pipe being full for more than {} millis", MAX_SLEEP_LOOP_MILLIS);
|
||||
throw (new IllegalStateException("Giving up adding record to pipe, due to pipe staying full too long."));
|
||||
}
|
||||
LOG.trace("Record pipe.add failed (due to full pipe). Blocking.");
|
||||
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class ReportUtils
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QReportView getSourceViewForPivotTableView(List<QReportView> views, QReportView pivotTableView) throws QReportingException
|
||||
{
|
||||
Optional<QReportView> sourceView = views.stream().filter(v -> v.getName().equals(pivotTableView.getPivotTableSourceViewName())).findFirst();
|
||||
if(sourceView.isEmpty())
|
||||
{
|
||||
throw (new QReportingException("Could not find data view [" + pivotTableView.getPivotTableSourceViewName() + "] for pivot table view [" + pivotTableView.getName() + "]"));
|
||||
}
|
||||
|
||||
return sourceView.get();
|
||||
}
|
||||
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
|
||||
|
||||
|
||||
import org.dhatim.fastexcel.StyleSetter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
|
||||
*******************************************************************************/
|
||||
public class PlainFastExcelStyler implements FastExcelStylerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** ... sorry, but adding this gives us test coverage on this class, even though
|
||||
** we're just deferring to super...
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void styleHeaderRow(StyleSetter headerRowStyle)
|
||||
{
|
||||
FastExcelStylerInterface.super.styleHeaderRow(headerRowStyle);
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
|
||||
|
||||
|
||||
import org.apache.poi.ss.usermodel.BorderStyle;
|
||||
import org.apache.poi.ss.usermodel.CreationHelper;
|
||||
import org.apache.poi.ss.usermodel.Font;
|
||||
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Version of POI excel styler that does bold headers and footers, with basic borders.
|
||||
*******************************************************************************/
|
||||
public class BoldHeaderAndFooterPoiExcelStyler implements PoiExcelStylerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public XSSFCellStyle createStyleForTitle(XSSFWorkbook workbook, CreationHelper createHelper)
|
||||
{
|
||||
Font font = workbook.createFont();
|
||||
font.setFontHeightInPoints((short) 14);
|
||||
font.setBold(true);
|
||||
|
||||
XSSFCellStyle cellStyle = workbook.createCellStyle();
|
||||
cellStyle.setFont(font);
|
||||
cellStyle.setAlignment(HorizontalAlignment.CENTER);
|
||||
|
||||
return (cellStyle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
|
||||
{
|
||||
Font font = workbook.createFont();
|
||||
font.setBold(true);
|
||||
|
||||
XSSFCellStyle cellStyle = workbook.createCellStyle();
|
||||
cellStyle.setFont(font);
|
||||
cellStyle.setBorderBottom(BorderStyle.THIN);
|
||||
|
||||
return (cellStyle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public XSSFCellStyle createStyleForFooter(XSSFWorkbook workbook, CreationHelper createHelper)
|
||||
{
|
||||
Font font = workbook.createFont();
|
||||
font.setBold(true);
|
||||
|
||||
XSSFCellStyle cellStyle = workbook.createCellStyle();
|
||||
cellStyle.setFont(font);
|
||||
cellStyle.setBorderTop(BorderStyle.THIN);
|
||||
cellStyle.setBorderBottom(BorderStyle.DOUBLE);
|
||||
|
||||
return (cellStyle);
|
||||
}
|
||||
|
||||
}
|
@ -1,819 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Serializable;
|
||||
import java.io.Writer;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ReportUtils;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableGroupBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable.PivotTableValue;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.poi.ss.SpreadsheetVersion;
|
||||
import org.apache.poi.ss.usermodel.CreationHelper;
|
||||
import org.apache.poi.ss.usermodel.DataConsolidateFunction;
|
||||
import org.apache.poi.ss.usermodel.DateUtil;
|
||||
import org.apache.poi.ss.util.AreaReference;
|
||||
import org.apache.poi.ss.util.CellReference;
|
||||
import org.apache.poi.ss.util.WorkbookUtil;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCell;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||
import org.apache.poi.xssf.usermodel.XSSFPivotTable;
|
||||
import org.apache.poi.xssf.usermodel.XSSFRow;
|
||||
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Excel export format implementation using POI library, but with modifications
|
||||
** to actually stream output rather than use any temp files.
|
||||
**
|
||||
** For a rough outline:
|
||||
** - create a basically empty Excel workbook using POI - empty meaning, without
|
||||
** data rows.
|
||||
** - have POI write that workbook out into a byte[] - which will be a zip full
|
||||
** of xml (e.g., xlsx).
|
||||
** - then open a new ZipOutputStream wrapper around the OutputStream we took in
|
||||
** as the report destination (e.g., streamed into storage or http output)
|
||||
** - Copy over all entries from the xlsx into our new zip-output-stream, other than
|
||||
** ones that are the actual sheets that we want to put data into.
|
||||
** - For the sheet entries, use the StreamedPoiSheetWriter class to write the
|
||||
** report's data directly out as Excel XML (not using POI).
|
||||
** - Pivot tables require a bit of an additional hack - to write a "pivot cache
|
||||
** definition", which, while we won't put all the data in it (because we'll tell
|
||||
** it refreshOnLoad="true"), we will at least need to know how many cols & rows
|
||||
** are in the data-sheet (which we wouldn't know until we streamed that sheet!)
|
||||
*******************************************************************************/
|
||||
public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ExcelPoiBasedStreamingExportStreamer.class);
|
||||
|
||||
private List<QReportView> views;
|
||||
private ExportInput exportInput;
|
||||
private List<QFieldMetaData> fields;
|
||||
private OutputStream outputStream;
|
||||
private ZipOutputStream zipOutputStream;
|
||||
|
||||
public static final String EXCEL_DATE_FORMAT = "yyyy-MM-dd";
|
||||
public static final String EXCEL_DATE_TIME_FORMAT = "yyyy-MM-dd H:mm:ss";
|
||||
|
||||
private PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
|
||||
private Map<String, String> excelCellFormats;
|
||||
private Map<String, XSSFCellStyle> styles = new HashMap<>();
|
||||
|
||||
private int rowNo = 0;
|
||||
private int sheetIndex = 1;
|
||||
|
||||
private Map<String, String> pivotViewToCacheDefinitionReferenceMap = new HashMap<>();
|
||||
|
||||
private Writer activeSheetWriter = null;
|
||||
private StreamedSheetWriter sheetWriter = null;
|
||||
|
||||
private QReportView currentView = null;
|
||||
private Map<String, List<QFieldMetaData>> fieldsPerView = new HashMap<>();
|
||||
private Map<String, Integer> rowsPerView = new HashMap<>();
|
||||
private Map<String, String> labelViewsByName = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ExcelPoiBasedStreamingExportStreamer()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void preRun(ReportDestination reportDestination, List<QReportView> views) throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
this.outputStream = reportDestination.getReportOutputStream();
|
||||
this.views = views;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// create 'template' workbook through poi - with sheets corresponding to our //
|
||||
// actual file this will be a zip file (stream), with entries for all of the //
|
||||
// files in the final xlsx but without any data, so it'll be small //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
XSSFWorkbook workbook = new XSSFWorkbook();
|
||||
createStyles(workbook);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for each of the sheets, create it in the workbook, and put a reference to it in the sheetMap //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Map<String, XSSFSheet> sheetMapByExcelReference = new HashMap<>();
|
||||
Map<String, XSSFSheet> sheetMapByViewName = new HashMap<>();
|
||||
|
||||
int sheetCounter = 1;
|
||||
for(QReportView view : views)
|
||||
{
|
||||
String label = Objects.requireNonNullElse(view.getLabel(), "Sheet " + sheetCounter);
|
||||
label = WorkbookUtil.createSafeSheetName(label);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// track the actually-used sheet labels (needed for referencing in pivot table generation) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
labelViewsByName.put(view.getName(), label);
|
||||
|
||||
XSSFSheet sheet = workbook.createSheet(label);
|
||||
String sheetReference = sheet.getPackagePart().getPartName().getName().substring(1);
|
||||
sheetMapByExcelReference.put(sheetReference, sheet);
|
||||
sheetMapByViewName.put(view.getName(), sheet);
|
||||
sheetCounter++;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// if any views are pivot tables, create them now //
|
||||
////////////////////////////////////////////////////
|
||||
List<String> pivotViewNames = new ArrayList<>();
|
||||
for(QReportView view : views)
|
||||
{
|
||||
if(ReportType.PIVOT.equals(view.getType()))
|
||||
{
|
||||
pivotViewNames.add(view.getName());
|
||||
|
||||
XSSFSheet pivotTableSheet = Objects.requireNonNull(sheetMapByViewName.get(view.getName()), "Could not get pivot table sheet view by name: " + view.getName());
|
||||
XSSFSheet dataSheet = Objects.requireNonNull(sheetMapByViewName.get(view.getPivotTableSourceViewName()), "Could not get pivot table source sheet by view name: " + view.getPivotTableSourceViewName());
|
||||
QReportView dataView = ReportUtils.getSourceViewForPivotTableView(views, view);
|
||||
createPivotTableTemplate(pivotTableSheet, view, dataSheet, dataView);
|
||||
}
|
||||
}
|
||||
Iterator<String> pivotViewNameIterator = pivotViewNames.iterator();
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// write that template worksheet zip out to byte array //
|
||||
/////////////////////////////////////////////////////////
|
||||
ByteArrayOutputStream templateBAOS = new ByteArrayOutputStream();
|
||||
workbook.write(templateBAOS);
|
||||
templateBAOS.close();
|
||||
byte[] templateBytes = templateBAOS.toByteArray();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// open up a zipOutputStream around the output stream that the report is to be written to. //
|
||||
// note: this stream is closed in the finish method - see more comments there. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
this.zipOutputStream = new ZipOutputStream(this.outputStream);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// copy over all the entries in the template zip that aren't the sheets into the output stream //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(templateBytes));
|
||||
ZipEntry zipTemplateEntry = null;
|
||||
byte[] buffer = new byte[2048];
|
||||
while((zipTemplateEntry = zipInputStream.getNextEntry()) != null)
|
||||
{
|
||||
if(zipTemplateEntry.getName().matches(".*/pivotCacheDefinition.*.xml"))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this zip entry is a pivotCacheDefinition, then don't write it to the output stream right now. //
|
||||
// instead, just map the pivot view's name to the zipTemplateEntry name //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!pivotViewNameIterator.hasNext())
|
||||
{
|
||||
throw new QReportingException("Found a pivot cache definition [" + zipTemplateEntry.getName() + "] in the template ZIP, but no (more) corresponding pivot view names");
|
||||
}
|
||||
|
||||
String pivotViewName = pivotViewNameIterator.next();
|
||||
LOG.info("Holding on a pivot cache definition zip template entry [" + pivotViewName + "] [" + zipTemplateEntry.getName() + "]...");
|
||||
pivotViewToCacheDefinitionReferenceMap.put(pivotViewName, zipTemplateEntry.getName());
|
||||
}
|
||||
else if(!sheetMapByExcelReference.containsKey(zipTemplateEntry.getName()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else, if we don't have this zipTemplateEntry name in our map of sheets, then this is a kinda "meta" //
|
||||
// file that we don't really care about (e.g., not our sheet data), so just copy it to the output stream. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.info("Copying zip template entry [" + zipTemplateEntry.getName() + "] to output stream");
|
||||
zipOutputStream.putNextEntry(new ZipEntry(zipTemplateEntry.getName()));
|
||||
|
||||
int length;
|
||||
while((length = zipInputStream.read(buffer)) > 0)
|
||||
{
|
||||
zipOutputStream.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
zipInputStream.closeEntry();
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// else - this is a sheet - so again, don't write it yet - stream its data below. //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.info("Skipping presumed sheet zip template entry [" + zipTemplateEntry.getName() + "] to output stream");
|
||||
}
|
||||
}
|
||||
|
||||
zipInputStream.close();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error preparing to generate spreadsheet", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void createPivotTableTemplate(XSSFSheet pivotTableSheet, QReportView pivotView, XSSFSheet dataSheet, QReportView dataView) throws QReportingException
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// write just enough data to the dataView's sheet so that we can refer to it for creating the pivot table. //
|
||||
// we need to do this, because POI will try to create the pivotCache referring to the data sheet, and if //
|
||||
// there isn't any data there, it'll crash. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
XSSFRow headerRow = dataSheet.createRow(0);
|
||||
int columnNo = 0;
|
||||
for(QReportField column : dataView.getColumns())
|
||||
{
|
||||
XSSFCell cell = headerRow.createCell(columnNo++);
|
||||
cell.setCellValue(QInstanceEnricher.nameToLabel(column.getName()));
|
||||
}
|
||||
|
||||
XSSFRow valuesRow = dataSheet.createRow(1);
|
||||
columnNo = 0;
|
||||
for(QReportField column : dataView.getColumns())
|
||||
{
|
||||
XSSFCell cell = valuesRow.createCell(columnNo++);
|
||||
cell.setCellValue("Value " + columnNo);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for this template version of the pivot table, tell it there are only 2 rows in the source sheet //
|
||||
// as that's all that we wrote above (a header and 1 fake value row) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
int rows = 2;
|
||||
String colsLetter = CellReference.convertNumToColString(dataView.getColumns().size() - 1);
|
||||
AreaReference source = new AreaReference("A1:" + colsLetter + rows, SpreadsheetVersion.EXCEL2007);
|
||||
CellReference position = new CellReference("A1");
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// tell poi all about our pivot table - rows, cols, and columns //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
XSSFPivotTable pivotTable = pivotTableSheet.createPivotTable(source, position, dataSheet);
|
||||
|
||||
for(PivotTableGroupBy row : CollectionUtils.nonNullList(pivotView.getPivotTableDefinition().getRows()))
|
||||
{
|
||||
int rowLabelColumnIndex = getColumnIndex(dataView.getColumns(), row.getFieldName());
|
||||
pivotTable.addRowLabel(rowLabelColumnIndex);
|
||||
}
|
||||
|
||||
for(PivotTableGroupBy column : CollectionUtils.nonNullList(pivotView.getPivotTableDefinition().getColumns()))
|
||||
{
|
||||
int colLabelColumnIndex = getColumnIndex(dataView.getColumns(), column.getFieldName());
|
||||
pivotTable.addColLabel(colLabelColumnIndex);
|
||||
}
|
||||
|
||||
for(PivotTableValue value : CollectionUtils.nonNullList(pivotView.getPivotTableDefinition().getValues()))
|
||||
{
|
||||
int columnLabelColumnIndex = getColumnIndex(dataView.getColumns(), value.getFieldName());
|
||||
|
||||
String labelPrefix = value.getFunction().name() + " of ";
|
||||
String label = labelPrefix + QInstanceEnricher.nameToLabel(value.getFieldName());
|
||||
String valueFormat = null;
|
||||
|
||||
Optional<QReportField> optSourceField = dataView.getColumns().stream().filter(c -> c.getName().equals(value.getFieldName())).findFirst();
|
||||
if(optSourceField.isPresent())
|
||||
{
|
||||
QReportField sourceField = optSourceField.get();
|
||||
|
||||
if(StringUtils.hasContent(sourceField.getLabel()))
|
||||
{
|
||||
label = labelPrefix + sourceField.getLabel();
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(sourceField.getDisplayFormat()))
|
||||
{
|
||||
valueFormat = DisplayFormat.getExcelFormat(sourceField.getDisplayFormat());
|
||||
}
|
||||
else
|
||||
{
|
||||
if(QFieldType.DATE.equals(sourceField.getType()))
|
||||
{
|
||||
valueFormat = EXCEL_DATE_FORMAT;
|
||||
}
|
||||
else if(QFieldType.DATE_TIME.equals(sourceField.getType()))
|
||||
{
|
||||
valueFormat = EXCEL_DATE_TIME_FORMAT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pivotTable.addColumnLabel(DataConsolidateFunction.valueOf(value.getFunction().name()), columnLabelColumnIndex, label, valueFormat);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private int getColumnIndex(List<QReportField> columns, String fieldName) throws QReportingException
|
||||
{
|
||||
for(int i = 0; i < columns.size(); i++)
|
||||
{
|
||||
if(columns.get(i).getName().equals(fieldName))
|
||||
{
|
||||
return (i);
|
||||
}
|
||||
}
|
||||
|
||||
throw (new QReportingException("Could not find column by name [" + fieldName + "]"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void createStyles(XSSFWorkbook workbook)
|
||||
{
|
||||
CreationHelper createHelper = workbook.getCreationHelper();
|
||||
|
||||
XSSFCellStyle dateStyle = workbook.createCellStyle();
|
||||
dateStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_FORMAT));
|
||||
styles.put("date", dateStyle);
|
||||
|
||||
XSSFCellStyle dateTimeStyle = workbook.createCellStyle();
|
||||
dateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
|
||||
styles.put("datetime", dateTimeStyle);
|
||||
|
||||
styles.put("title", poiExcelStylerInterface.createStyleForTitle(workbook, createHelper));
|
||||
styles.put("header", poiExcelStylerInterface.createStyleForHeader(workbook, createHelper));
|
||||
styles.put("footer", poiExcelStylerInterface.createStyleForFooter(workbook, createHelper));
|
||||
|
||||
XSSFCellStyle footerDateStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper);
|
||||
footerDateStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_FORMAT));
|
||||
styles.put("footer-date", footerDateStyle);
|
||||
|
||||
XSSFCellStyle footerDateTimeStyle = poiExcelStylerInterface.createStyleForFooter(workbook, createHelper);
|
||||
footerDateTimeStyle.setDataFormat(createHelper.createDataFormat().getFormat(EXCEL_DATE_TIME_FORMAT));
|
||||
styles.put("footer-datetime", footerDateTimeStyle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Starts a new worksheet in the current workbook. Can be called multiple times.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
/////////////////////////////////////////
|
||||
// close previous sheet if one is open //
|
||||
/////////////////////////////////////////
|
||||
closeLastSheetIfOpen();
|
||||
|
||||
if(currentView != null)
|
||||
{
|
||||
this.rowsPerView.put(currentView.getName(), rowNo);
|
||||
}
|
||||
|
||||
this.currentView = view;
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
this.rowNo = 0;
|
||||
|
||||
this.fieldsPerView.put(view.getName(), fields);
|
||||
|
||||
//////////////////////////////////////////
|
||||
// start the new sheet as: //
|
||||
// - a new entry in the zipOutputStream //
|
||||
// - with a new output stream writer //
|
||||
// - and with a SpreadsheetWriter //
|
||||
//////////////////////////////////////////
|
||||
zipOutputStream.putNextEntry(new ZipEntry("xl/worksheets/sheet" + this.sheetIndex++ + ".xml"));
|
||||
activeSheetWriter = new OutputStreamWriter(zipOutputStream);
|
||||
sheetWriter = new StreamedSheetWriter(activeSheetWriter);
|
||||
|
||||
if(ReportType.PIVOT.equals(view.getType()))
|
||||
{
|
||||
writePivotTable(view, ReportUtils.getSourceViewForPivotTableView(views, view));
|
||||
}
|
||||
else
|
||||
{
|
||||
sheetWriter.beginSheet();
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// put the title and header rows in the sheet //
|
||||
////////////////////////////////////////////////
|
||||
writeTitleAndHeader();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error starting worksheet", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void writeTitleAndHeader() throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
///////////////
|
||||
// title row //
|
||||
///////////////
|
||||
if(StringUtils.hasContent(exportInput.getTitleRow()))
|
||||
{
|
||||
sheetWriter.insertRow(rowNo++);
|
||||
sheetWriter.createCell(0, exportInput.getTitleRow(), styles.get("title").getIndex());
|
||||
sheetWriter.endRow();
|
||||
}
|
||||
|
||||
////////////////
|
||||
// header row //
|
||||
////////////////
|
||||
if(exportInput.getIncludeHeaderRow())
|
||||
{
|
||||
sheetWriter.insertRow(rowNo++);
|
||||
XSSFCellStyle headerStyle = styles.get("header");
|
||||
|
||||
int col = 0;
|
||||
for(QFieldMetaData column : fields)
|
||||
{
|
||||
sheetWriter.createCell(col, column.getLabel(), headerStyle.getIndex());
|
||||
col++;
|
||||
}
|
||||
|
||||
sheetWriter.endRow();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error starting Excel report"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void addRecords(List<QRecord> qRecords) throws QReportingException
|
||||
{
|
||||
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||
|
||||
try
|
||||
{
|
||||
for(QRecord qRecord : qRecords)
|
||||
{
|
||||
writeRecord(qRecord);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Exception generating excel file", e);
|
||||
try
|
||||
{
|
||||
outputStream.close();
|
||||
}
|
||||
catch(IOException ex)
|
||||
{
|
||||
LOG.warn("Secondary error closing excel output stream", e);
|
||||
}
|
||||
|
||||
throw (new QReportingException("Error generating Excel report", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void writeRecord(QRecord qRecord) throws IOException
|
||||
{
|
||||
writeRecord(qRecord, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void writeRecord(QRecord qRecord, boolean isFooter) throws IOException
|
||||
{
|
||||
sheetWriter.insertRow(rowNo++);
|
||||
|
||||
int styleIndex = -1;
|
||||
int dateStyleIndex = styles.get("date").getIndex();
|
||||
int dateTimeStyleIndex = styles.get("datetime").getIndex();
|
||||
if(isFooter)
|
||||
{
|
||||
styleIndex = styles.get("footer").getIndex();
|
||||
dateStyleIndex = styles.get("footer-date").getIndex();
|
||||
dateTimeStyleIndex = styles.get("footer-datetime").getIndex();
|
||||
}
|
||||
|
||||
int col = 0;
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
Serializable value = qRecord.getValue(field.getName());
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
if(value instanceof String s)
|
||||
{
|
||||
sheetWriter.createCell(col, s, styleIndex);
|
||||
}
|
||||
else if(value instanceof Number n)
|
||||
{
|
||||
sheetWriter.createCell(col, n.doubleValue(), styleIndex);
|
||||
|
||||
if(excelCellFormats != null)
|
||||
{
|
||||
String format = excelCellFormats.get(field.getName());
|
||||
if(format != null)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - so - for this streamed/zip approach, we need to know all styles before we start //
|
||||
// any sheets. but, right now Report action only calls us with per-sheet styles when //
|
||||
// it's starting individual sheets. so, we can't quite support this at this time. //
|
||||
// "just" need to change GenerateReportAction to look up all cell formats for all sheets //
|
||||
// before preRun is called... and change all existing streamer classes to handle that too //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// worksheet.style(rowNo, col).format(format).set();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(value instanceof Boolean b)
|
||||
{
|
||||
sheetWriter.createCell(col, b, styleIndex);
|
||||
}
|
||||
else if(value instanceof Date d)
|
||||
{
|
||||
sheetWriter.createCell(col, DateUtil.getExcelDate(d), dateStyleIndex);
|
||||
}
|
||||
else if(value instanceof LocalDate d)
|
||||
{
|
||||
sheetWriter.createCell(col, DateUtil.getExcelDate(d), dateStyleIndex);
|
||||
}
|
||||
else if(value instanceof LocalDateTime d)
|
||||
{
|
||||
sheetWriter.createCell(col, DateUtil.getExcelDate(d), dateStyleIndex);
|
||||
}
|
||||
else if(value instanceof ZonedDateTime d)
|
||||
{
|
||||
sheetWriter.createCell(col, DateUtil.getExcelDate(d.toLocalDateTime()), dateTimeStyleIndex);
|
||||
}
|
||||
else if(value instanceof Instant i)
|
||||
{
|
||||
sheetWriter.createCell(col, DateUtil.getExcelDate(i.atZone(ZoneId.systemDefault()).toLocalDateTime()), dateTimeStyleIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
sheetWriter.createCell(col, ValueUtils.getValueAsString(value), styleIndex);
|
||||
}
|
||||
}
|
||||
|
||||
col++;
|
||||
}
|
||||
|
||||
sheetWriter.endRow();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addTotalsRow(QRecord record) throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
writeRecord(record, true);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error adding totals row", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void finish() throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////
|
||||
// close the last open sheet if one is open //
|
||||
//////////////////////////////////////////////
|
||||
closeLastSheetIfOpen();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// so, we DO need a close here, on the zipOutputStream, to finish its "zippiness". //
|
||||
// even though, doing so also closes the outputStream from the caller that this //
|
||||
// zipOutputStream is wrapping (and the caller will likely call close on too)... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
zipOutputStream.close();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error finishing Excel report", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void closeLastSheetIfOpen() throws IOException
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we have an active sheet writer: //
|
||||
// - end the current sheet in the spreadsheet writer (write some closing xml, unless it's a pivot!) //
|
||||
// - flush the contents through the activeSheetWriter //
|
||||
// - close the zip entry in the output stream. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(activeSheetWriter != null)
|
||||
{
|
||||
if(!ReportType.PIVOT.equals(currentView.getType()))
|
||||
{
|
||||
sheetWriter.endSheet();
|
||||
}
|
||||
|
||||
activeSheetWriter.flush();
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** display formats is a map of field name to Excel format strings (e.g., $#,##0.00)
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void setDisplayFormats(Map<String, String> displayFormats)
|
||||
{
|
||||
this.excelCellFormats = new HashMap<>();
|
||||
for(Map.Entry<String, String> entry : displayFormats.entrySet())
|
||||
{
|
||||
String excelFormat = DisplayFormat.getExcelFormat(entry.getValue());
|
||||
if(excelFormat != null)
|
||||
{
|
||||
excelCellFormats.put(entry.getKey(), excelFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void writePivotTable(QReportView pivotTableView, QReportView dataView) throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// write the xml file that is the pivot table sheet. //
|
||||
// note that the ZipEntry here will have been started above in the start method //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
activeSheetWriter.write("""
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mx="http://schemas.microsoft.com/office/mac/excel/2008/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mv="urn:schemas-microsoft-com:mac:vml" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main">
|
||||
<sheetPr>
|
||||
<outlinePr summaryBelow="0" summaryRight="0"/>
|
||||
</sheetPr>
|
||||
<sheetViews>
|
||||
<sheetView workbookViewId="0"/>
|
||||
</sheetViews>
|
||||
<sheetFormatPr customHeight="1" defaultColWidth="14.43" defaultRowHeight="15.0"/>
|
||||
<sheetData>
|
||||
<row r="1"/>
|
||||
</sheetData>
|
||||
</worksheet>
|
||||
""");
|
||||
activeSheetWriter.flush();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// start a new zip entry, for this pivot view's cacheDefinition reference //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
zipOutputStream.putNextEntry(new ZipEntry(pivotViewToCacheDefinitionReferenceMap.get(pivotTableView.getName())));
|
||||
|
||||
/////////////////////////////////////////////////////////
|
||||
// prepare the xml for each field (e.g., w/ its label) //
|
||||
/////////////////////////////////////////////////////////
|
||||
List<String> cachedFieldElements = new ArrayList<>();
|
||||
for(QFieldMetaData column : this.fieldsPerView.get(dataView.getName()))
|
||||
{
|
||||
cachedFieldElements.add(String.format("""
|
||||
<cacheField numFmtId="0" name="%s">
|
||||
<sharedItems/>
|
||||
</cacheField>
|
||||
""", column.getLabel()));
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// write the xml file that is the pivot cache definition (structure only, no data) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
activeSheetWriter = new OutputStreamWriter(zipOutputStream);
|
||||
activeSheetWriter.write(String.format("""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" createdVersion="3" minRefreshableVersion="3" refreshedVersion="3" refreshedBy="Apache POI" refreshedDate="1.711395767702E12" refreshOnLoad="true" r:id="rId1">
|
||||
<cacheSource type="worksheet">
|
||||
<worksheetSource sheet="%s" ref="A1:%s%d"/>
|
||||
</cacheSource>
|
||||
<cacheFields count="%d">
|
||||
%s
|
||||
</cacheFields>
|
||||
</pivotCacheDefinition>
|
||||
""",
|
||||
StreamedSheetWriter.cleanseValue(labelViewsByName.get(dataView.getName())),
|
||||
CellReference.convertNumToColString(dataView.getColumns().size() - 1),
|
||||
rowsPerView.get(dataView.getName()),
|
||||
dataView.getColumns().size(),
|
||||
StringUtils.join("\n", cachedFieldElements)));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error writing pivot table", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected PoiExcelStylerInterface getStylerInterface()
|
||||
{
|
||||
return (new PlainPoiExcelStyler());
|
||||
}
|
||||
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
|
||||
|
||||
|
||||
import org.apache.poi.ss.usermodel.CreationHelper;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
|
||||
*******************************************************************************/
|
||||
public class PlainPoiExcelStyler implements PoiExcelStylerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** ... sorry, but adding this gives us test coverage on this class, even though
|
||||
** we're just deferring to super...
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
|
||||
{
|
||||
return PoiExcelStylerInterface.super.createStyleForHeader(workbook, createHelper);
|
||||
}
|
||||
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
|
||||
|
||||
|
||||
import org.apache.poi.ss.usermodel.CreationHelper;
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface for classes that know how to apply styles to an Excel stream being
|
||||
** built by POI.
|
||||
*******************************************************************************/
|
||||
public interface PoiExcelStylerInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default XSSFCellStyle createStyleForTitle(XSSFWorkbook workbook, CreationHelper createHelper)
|
||||
{
|
||||
return (workbook.createCellStyle());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default XSSFCellStyle createStyleForHeader(XSSFWorkbook workbook, CreationHelper createHelper)
|
||||
{
|
||||
return (workbook.createCellStyle());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default XSSFCellStyle createStyleForFooter(XSSFWorkbook workbook, CreationHelper createHelper)
|
||||
{
|
||||
return (workbook.createCellStyle());
|
||||
}
|
||||
}
|
@ -1,242 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.poi;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.poi.ss.util.CellReference;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Write excel formatted XML to a Writer.
|
||||
** Originally from https://coderanch.com/t/548897/java/Generate-large-excel-POI
|
||||
*******************************************************************************/
|
||||
public class StreamedSheetWriter
|
||||
{
|
||||
private final Writer writer;
|
||||
private int rowNo;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public StreamedSheetWriter(Writer writer)
|
||||
{
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void beginSheet() throws IOException
|
||||
{
|
||||
writer.write("""
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<sheetData>""");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void endSheet() throws IOException
|
||||
{
|
||||
writer.write("""
|
||||
</sheetData>
|
||||
</worksheet>""");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert a new row
|
||||
**
|
||||
** @param rowNo 0-based row number
|
||||
*******************************************************************************/
|
||||
public void insertRow(int rowNo) throws IOException
|
||||
{
|
||||
writer.write("<row r=\"" + (rowNo + 1) + "\">\n");
|
||||
this.rowNo = rowNo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert row end marker
|
||||
*******************************************************************************/
|
||||
public void endRow() throws IOException
|
||||
{
|
||||
writer.write("</row>\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void createCell(int columnIndex, String value, int styleIndex) throws IOException
|
||||
{
|
||||
String ref = new CellReference(rowNo, columnIndex).formatAsString();
|
||||
writer.write("<c r=\"" + ref + "\" t=\"inlineStr\"");
|
||||
if(styleIndex != -1)
|
||||
{
|
||||
writer.write(" s=\"" + styleIndex + "\"");
|
||||
}
|
||||
|
||||
String cleanValue = cleanseValue(value);
|
||||
|
||||
writer.write(">");
|
||||
writer.write("<is><t>" + cleanValue + "</t></is>");
|
||||
writer.write("</c>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String cleanseValue(String value)
|
||||
{
|
||||
if(value != null)
|
||||
{
|
||||
StringBuilder rs = new StringBuilder();
|
||||
for(int i = 0; i < value.length(); i++)
|
||||
{
|
||||
char c = value.charAt(i);
|
||||
if(c == '&')
|
||||
{
|
||||
rs.append("&");
|
||||
}
|
||||
else if(c == '<')
|
||||
{
|
||||
rs.append("<");
|
||||
}
|
||||
else if(c == '>')
|
||||
{
|
||||
rs.append(">");
|
||||
}
|
||||
else if(c == '\'')
|
||||
{
|
||||
rs.append("'");
|
||||
}
|
||||
else if(c == '"')
|
||||
{
|
||||
rs.append(""");
|
||||
}
|
||||
else if (c < 32 && c != '\t' && c != '\n')
|
||||
{
|
||||
rs.append(' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
rs.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Integer> m = new HashMap();
|
||||
m.computeIfAbsent("s", (s) -> 3);
|
||||
|
||||
value = rs.toString();
|
||||
}
|
||||
|
||||
return (value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void createCell(int columnIndex, String value) throws IOException
|
||||
{
|
||||
createCell(columnIndex, value, -1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void createCell(int columnIndex, double value, int styleIndex) throws IOException
|
||||
{
|
||||
String ref = new CellReference(rowNo, columnIndex).formatAsString();
|
||||
writer.write("<c r=\"" + ref + "\" t=\"n\"");
|
||||
if(styleIndex != -1)
|
||||
{
|
||||
writer.write(" s=\"" + styleIndex + "\"");
|
||||
}
|
||||
writer.write(">");
|
||||
writer.write("<v>" + value + "</v>");
|
||||
writer.write("</c>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void createCell(int columnIndex, double value) throws IOException
|
||||
{
|
||||
createCell(columnIndex, value, -1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void createCell(int columnIndex, Boolean value) throws IOException
|
||||
{
|
||||
createCell(columnIndex, value, -1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void createCell(int columnIndex, Boolean value, int styleIndex) throws IOException
|
||||
{
|
||||
String ref = new CellReference(rowNo, columnIndex).formatAsString();
|
||||
writer.write("<c r=\"" + ref + "\" t=\"b\"");
|
||||
if(styleIndex != -1)
|
||||
{
|
||||
writer.write(" s=\"" + styleIndex + "\"");
|
||||
}
|
||||
writer.write(">");
|
||||
if(value != null)
|
||||
{
|
||||
writer.write("<v>" + (value ? 1 : 0) + "</v>");
|
||||
}
|
||||
writer.write("</c>");
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
|
||||
|
||||
|
||||
import org.dhatim.fastexcel.BorderSide;
|
||||
@ -30,7 +30,7 @@ import org.dhatim.fastexcel.StyleSetter;
|
||||
/*******************************************************************************
|
||||
** Version of excel styler that does bold headers and footers, with basic borders.
|
||||
*******************************************************************************/
|
||||
public class BoldHeaderAndFooterFastExcelStyler implements FastExcelStylerInterface
|
||||
public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
@ -60,9 +60,6 @@ public class BoldHeaderAndFooterFastExcelStyler implements FastExcelStylerInterf
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void styleTotalsRow(StyleSetter totalsRowStyle)
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
|
||||
|
||||
|
||||
import org.dhatim.fastexcel.StyleSetter;
|
||||
@ -29,7 +29,7 @@ import org.dhatim.fastexcel.StyleSetter;
|
||||
** Interface for classes that know how to apply styles to an Excel stream being
|
||||
** built by fastexcel.
|
||||
*******************************************************************************/
|
||||
public interface FastExcelStylerInterface
|
||||
public interface ExcelStylerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
@ -19,15 +19,13 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.dashboard;
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Possible types for widget dropdowns
|
||||
**
|
||||
** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
|
||||
*******************************************************************************/
|
||||
public enum WidgetDropdownType
|
||||
public class PlainExcelStyler implements ExcelStylerInterface
|
||||
{
|
||||
POSSIBLE_VALUE_SOURCE,
|
||||
DATE_PICKER
|
||||
|
||||
}
|
@ -470,7 +470,7 @@ public class DeleteAction
|
||||
QRecord recordWithError = new QRecord();
|
||||
recordsWithErrors.add(recordWithError);
|
||||
recordWithError.setValue(primaryKeyField.getName(), primaryKeyValue);
|
||||
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + Objects.requireNonNullElse(primaryKeyField.getLabel(), primaryKeyField.getName()) + " = " + primaryKeyValue));
|
||||
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + primaryKeyField.getLabel() + " = " + primaryKeyValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.DuplicateKeyBadInputStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
@ -415,7 +414,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
|
||||
if(keyValues.isPresent() && (existingKeys.get(uniqueKey).contains(keyValues.get()) || keysInThisList.get(uniqueKey).contains(keyValues.get())))
|
||||
{
|
||||
record.addError(new DuplicateKeyBadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
|
||||
record.addError(new BadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
|
||||
foundDupe = true;
|
||||
break;
|
||||
}
|
||||
|
@ -169,7 +169,6 @@ public class QueryAction
|
||||
nextLevelQueryInput.setTableName(association.getAssociatedTableName());
|
||||
nextLevelQueryInput.setIncludeAssociations(true);
|
||||
nextLevelQueryInput.setAssociationNamesToInclude(buildNextLevelAssociationNamesToInclude(association.getName(), queryInput.getAssociationNamesToInclude()));
|
||||
nextLevelQueryInput.setTransaction(queryInput.getTransaction());
|
||||
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
nextLevelQueryInput.setFilter(filter);
|
||||
|
@ -47,7 +47,6 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -80,11 +79,9 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
||||
|
||||
try
|
||||
{
|
||||
QTableMetaData table = input.getTable();
|
||||
UniqueKey uniqueKey = input.getKey();
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
boolean allowNullKeyValuesToEqual = BooleanUtils.isTrue(input.getAllowNullKeyValuesToEqual());
|
||||
|
||||
QTableMetaData table = input.getTable();
|
||||
UniqueKey uniqueKey = input.getKey();
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
if(transaction == null)
|
||||
{
|
||||
transaction = QBackendTransaction.openFor(new InsertInput(input.getTableName()));
|
||||
@ -101,11 +98,10 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
||||
// originally it was thought that we'd need to pass the filter in here //
|
||||
// but, it's been decided not to. the filter only applies to what we can delete //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
Map<List<Serializable>, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey, allowNullKeyValuesToEqual);
|
||||
|
||||
Map<List<Serializable>, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey);
|
||||
for(QRecord record : page)
|
||||
{
|
||||
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
|
||||
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
|
||||
if(keyValues.isPresent())
|
||||
{
|
||||
if(existingKeys.containsKey(keyValues.get()))
|
||||
@ -157,17 +153,6 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
||||
output.setDeleteOutput(deleteOutput);
|
||||
}
|
||||
|
||||
if(input.getSetPrimaryKeyInInsertedRecords())
|
||||
{
|
||||
for(int i = 0; i < insertList.size(); i++)
|
||||
{
|
||||
if(i < insertOutput.getRecords().size())
|
||||
{
|
||||
insertList.get(i).setValue(primaryKeyField, insertOutput.getRecords().get(i).getValue(primaryKeyField));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(weOwnTheTransaction)
|
||||
{
|
||||
transaction.commit();
|
||||
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.QStorageInterface;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Action to do (generally, "mass") storage operations in a backend.
|
||||
**
|
||||
** e.g., store a (potentially large) file - specifically - by working with it
|
||||
** as either an InputStream or OutputStream.
|
||||
**
|
||||
** May not be implemented in all backends.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class StorageAction
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public OutputStream createOutputStream(StorageInput storageInput) throws QException
|
||||
{
|
||||
QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
|
||||
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
|
||||
return (storageInterface.createOutputStream(storageInput));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public InputStream getInputStream(StorageInput storageInput) throws QException
|
||||
{
|
||||
QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
|
||||
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
|
||||
return (storageInterface.getInputStream(storageInput));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QBackendModuleInterface preAction(StorageInput storageInput) throws QException
|
||||
{
|
||||
ActionHelper.validateSession(storageInput);
|
||||
|
||||
if(storageInput.getTableName() == null)
|
||||
{
|
||||
throw (new QException("Table name was not specified in storage input"));
|
||||
}
|
||||
|
||||
QTableMetaData table = storageInput.getTable();
|
||||
if(table == null)
|
||||
{
|
||||
throw (new QException("A table named [" + storageInput.getTableName() + "] was not found in the active QInstance"));
|
||||
}
|
||||
|
||||
QBackendMetaData backend = storageInput.getBackend();
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
|
||||
return (qModule);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void makePublic(StorageInput storageInput) throws QException
|
||||
{
|
||||
QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
|
||||
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
|
||||
storageInterface.makePublic(storageInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getDownloadURL(StorageInput storageInput) throws QException
|
||||
{
|
||||
QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
|
||||
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
|
||||
return (storageInterface.getDownloadURL(storageInput));
|
||||
}
|
||||
}
|
@ -68,7 +68,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.NotFoundStatusMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
@ -394,12 +393,7 @@ public class UpdateAction
|
||||
QRecord oldRecord = lookedUpRecords.get(value);
|
||||
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
|
||||
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
|
||||
|
||||
List<QErrorMessage> errors = ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
errors.forEach(e -> record.addError(e));
|
||||
}
|
||||
ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, record, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,78 +214,71 @@ public class QueryStatManager
|
||||
*******************************************************************************/
|
||||
public void add(QueryStat queryStat)
|
||||
{
|
||||
try
|
||||
if(queryStat == null)
|
||||
{
|
||||
if(queryStat == null)
|
||||
return;
|
||||
}
|
||||
|
||||
if(active)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set fields that we need to capture now (rather than when the thread to store runs) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(queryStat.getFirstResultTimestamp() == null)
|
||||
{
|
||||
queryStat.setFirstResultTimestamp(Instant.now());
|
||||
}
|
||||
|
||||
if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null)
|
||||
{
|
||||
long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli();
|
||||
queryStat.setFirstResultMillis((int) millis);
|
||||
}
|
||||
|
||||
if(queryStat.getFirstResultMillis() != null && queryStat.getFirstResultMillis() < minMillisToStore)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// discard this record if it's under the min millis setting //
|
||||
//////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
if(active)
|
||||
if(queryStat.getSessionId() == null && QContext.getQSession() != null)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set fields that we need to capture now (rather than when the thread to store runs) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(queryStat.getFirstResultTimestamp() == null)
|
||||
{
|
||||
queryStat.setFirstResultTimestamp(Instant.now());
|
||||
}
|
||||
queryStat.setSessionId(QContext.getQSession().getUuid());
|
||||
}
|
||||
|
||||
if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null)
|
||||
if(queryStat.getAction() == null)
|
||||
{
|
||||
if(!QContext.getActionStack().isEmpty())
|
||||
{
|
||||
long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli();
|
||||
queryStat.setFirstResultMillis((int) millis);
|
||||
queryStat.setAction(QContext.getActionStack().peek().getActionIdentity());
|
||||
}
|
||||
|
||||
if(queryStat.getFirstResultMillis() != null && queryStat.getFirstResultMillis() < minMillisToStore)
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// discard this record if it's under the min millis setting //
|
||||
//////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
if(queryStat.getSessionId() == null && QContext.getQSession() != null)
|
||||
{
|
||||
queryStat.setSessionId(QContext.getQSession().getUuid());
|
||||
}
|
||||
|
||||
if(queryStat.getAction() == null)
|
||||
{
|
||||
if(QContext.getActionStack() != null && !QContext.getActionStack().isEmpty())
|
||||
boolean expected = false;
|
||||
Exception e = new Exception("Unexpected empty action stack");
|
||||
for(StackTraceElement stackTraceElement : e.getStackTrace())
|
||||
{
|
||||
queryStat.setAction(QContext.getActionStack().peek().getActionIdentity());
|
||||
}
|
||||
else
|
||||
{
|
||||
boolean expected = false;
|
||||
Exception e = new Exception("Unexpected empty action stack");
|
||||
for(StackTraceElement stackTraceElement : e.getStackTrace())
|
||||
String className = stackTraceElement.getClassName();
|
||||
if(className.contains(QueryStatManagerInsertJob.class.getName()))
|
||||
{
|
||||
String className = stackTraceElement.getClassName();
|
||||
if(className.contains(QueryStatManagerInsertJob.class.getName()))
|
||||
{
|
||||
expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!expected)
|
||||
{
|
||||
LOG.debug(e);
|
||||
expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(this)
|
||||
{
|
||||
queryStats.add(queryStat);
|
||||
if(!expected)
|
||||
{
|
||||
LOG.debug(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Error adding query stat", e);
|
||||
|
||||
synchronized(this)
|
||||
{
|
||||
queryStats.add(queryStat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ public class UniqueKeyHelper
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey, boolean allowNullKeyValuesToEqual) throws QException
|
||||
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
|
||||
{
|
||||
List<String> ukFieldNames = uniqueKey.getFieldNames();
|
||||
Map<List<Serializable>, Serializable> existingRecords = new HashMap<>();
|
||||
@ -112,7 +112,7 @@ public class UniqueKeyHelper
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
|
||||
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record);
|
||||
if(keyValues.isPresent())
|
||||
{
|
||||
existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField()));
|
||||
@ -128,17 +128,7 @@ public class UniqueKeyHelper
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
|
||||
{
|
||||
return (getExistingKeys(transaction, table, recordList, uniqueKey, false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record, boolean allowNullKeyValuesToEqual)
|
||||
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -148,19 +138,7 @@ public class UniqueKeyHelper
|
||||
QFieldMetaData field = table.getField(fieldName);
|
||||
Serializable value = record.getValue(fieldName);
|
||||
Serializable typedValue = ValueUtils.getValueAsFieldType(field.getType(), value);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// if null value, look at flag to determine if a null should be used (which will //
|
||||
// allow keys to match), or a NullUniqueKeyValue, (which will never match) //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
if(typedValue == null)
|
||||
{
|
||||
keyValues.add(allowNullKeyValuesToEqual ? null : new NullUniqueKeyValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
keyValues.add(typedValue);
|
||||
}
|
||||
keyValues.add(typedValue == null ? new NullUniqueKeyValue() : typedValue);
|
||||
}
|
||||
return (Optional.of(keyValues));
|
||||
}
|
||||
@ -172,16 +150,6 @@ public class UniqueKeyHelper
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record)
|
||||
{
|
||||
return (getKeyValues(table, uniqueKey, record, false));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** To make a list of unique key values here behave like they do in an RDBMS
|
||||
** (which is what we're trying to mimic - which is - 2 null values in a field
|
||||
|
@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.actions.tables.helpers;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -43,16 +42,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
@ -85,273 +81,160 @@ public class ValidateRecordSecurityLockHelper
|
||||
*******************************************************************************/
|
||||
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
|
||||
{
|
||||
MultiRecordSecurityLock locksToCheck = getRecordSecurityLocks(table, action);
|
||||
if(locksToCheck == null || CollectionUtils.nullSafeIsEmpty(locksToCheck.getLocks()))
|
||||
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table, action);
|
||||
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we will be relying on primary keys being set in records - but (at least for inserts) //
|
||||
// we might not have pkeys - so make them up (and clear them out at the end) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
Map<Serializable, QRecord> madeUpPrimaryKeys = makeUpPrimaryKeysIfNeeded(records, table);
|
||||
|
||||
////////////////////////////////
|
||||
// actually check lock values //
|
||||
////////////////////////////////
|
||||
Map<Serializable, RecordWithErrors> errorRecords = new HashMap<>();
|
||||
evaluateRecordLocks(table, records, action, locksToCheck, errorRecords, new ArrayList<>());
|
||||
|
||||
/////////////////////////////////
|
||||
// propagate errors to records //
|
||||
/////////////////////////////////
|
||||
for(RecordWithErrors recordWithErrors : errorRecords.values())
|
||||
for(RecordSecurityLock recordSecurityLock : locksToCheck)
|
||||
{
|
||||
recordWithErrors.propagateErrorsToRecord(locksToCheck);
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
// remove made-up primary keys //
|
||||
/////////////////////////////////
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
for(QRecord record : madeUpPrimaryKeys.values())
|
||||
{
|
||||
record.setValue(primaryKeyField, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a list of `records` from a `table`, and a given `action`, evaluate a
|
||||
** `recordSecurityLock` (which may be a multi-lock) - populating the input map
|
||||
** of `errorRecords` - key'ed by primary key value (real or made up), with
|
||||
** error messages existing in a tree, with positions matching the multi-lock
|
||||
** tree that we're navigating, as tracked by `treePosition`.
|
||||
**
|
||||
** Recursively processes multi-locks (and the top-level call is always with a
|
||||
** multi-lock - as the table's recordLocks list converted to an AND-multi-lock).
|
||||
**
|
||||
** Of note - for the case of READ_WRITE locks, we're only evaluating the values
|
||||
** on the record, to see if they're allowed for us to store (because if we didn't
|
||||
** have the key, we wouldn't have been able to read the value (which is verified
|
||||
** outside of here, in UpdateAction/DeleteAction).
|
||||
**
|
||||
** BUT - WRITE locks - in their case, we read the record no matter what, and in
|
||||
** here we need to verify we have a key that allows us to WRITE the record.
|
||||
*******************************************************************************/
|
||||
private static void evaluateRecordLocks(QTableMetaData table, List<QRecord> records, Action action, RecordSecurityLock recordSecurityLock, Map<Serializable, RecordWithErrors> errorRecords, List<Integer> treePosition) throws QException
|
||||
{
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
{
|
||||
/////////////////////////////////////////////
|
||||
// for multi-locks, make recursive descent //
|
||||
/////////////////////////////////////////////
|
||||
int i = 0;
|
||||
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
|
||||
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
treePosition.add(i);
|
||||
evaluateRecordLocks(table, records, action, childLock, errorRecords, treePosition);
|
||||
treePosition.remove(treePosition.size() - 1);
|
||||
i++;
|
||||
}
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// handle the value being in the table we're inserting/updating (e.g., no join) //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this lock has an all-access key, and the user has that key, then there can't be any errors here, so return early //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// proceed w/ non-multi locks //
|
||||
////////////////////////////////
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// handle the value being in the table we're inserting/updating (e.g., no join) //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
|
||||
for(QRecord record : records)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
|
||||
// So if we're not updating the security field, then no error can come from it! //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
|
||||
// So if we're not updating the security field, then no error can come from it! //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
Serializable recordSecurityValue = record.getValue(field.getName());
|
||||
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action);
|
||||
if(CollectionUtils.nullSafeHasContents(recordErrors))
|
||||
{
|
||||
errorRecords.computeIfAbsent(record.getValue(primaryKeyField), (k) -> new RecordWithErrors(record)).addAll(recordErrors, treePosition);
|
||||
Serializable recordSecurityValue = record.getValue(field.getName());
|
||||
validateRecordSecurityValue(table, record, recordSecurityLock, recordSecurityValue, field.getType(), action);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
|
||||
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
|
||||
|
||||
////////////////////////////////
|
||||
// todo probably, but more... //
|
||||
////////////////////////////////
|
||||
// if(leftMostJoin.getLeftTable().equals(table.getName()))
|
||||
// {
|
||||
// leftMostJoin = leftMostJoin.flip();
|
||||
// }
|
||||
|
||||
QTableMetaData rightMostJoinTable = QContext.getQInstance().getTable(rightMostJoin.getRightTable());
|
||||
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
|
||||
|
||||
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set up a query for joined records //
|
||||
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(leftMostJoin.getLeftTable());
|
||||
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
queryInput.setFilter(filter);
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
|
||||
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
|
||||
QTableMetaData rightMostJoinTable = QContext.getQInstance().getTable(rightMostJoin.getRightTable());
|
||||
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
|
||||
|
||||
for(String joinName : recordSecurityLock.getJoinNameChain())
|
||||
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
|
||||
{
|
||||
///////////////////////////////////////
|
||||
// we don't need the right-most join //
|
||||
///////////////////////////////////////
|
||||
if(!joinName.equals(rightMostJoin.getName()))
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set up a query for joined records //
|
||||
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(leftMostJoin.getLeftTable());
|
||||
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
queryInput.setFilter(filter);
|
||||
|
||||
for(String joinName : recordSecurityLock.getJoinNameChain())
|
||||
{
|
||||
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
|
||||
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
|
||||
// also build up the query's sub-filters here (only adding them if they're unique). //
|
||||
// e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ListingHash<List<Serializable>, QRecord> inputRecordMapByJoinFields = new ListingHash<>();
|
||||
for(QRecord inputRecord : inputRecordPage)
|
||||
{
|
||||
List<Serializable> inputRecordJoinValues = new ArrayList<>();
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
|
||||
boolean updatingAnyLockJoinFields = false;
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
QFieldType type = rightMostJoinTable.getField(joinOn.getRightField()).getType();
|
||||
Serializable inputRecordValue = ValueUtils.getValueAsFieldType(type, inputRecord.getValue(joinOn.getRightField()));
|
||||
inputRecordJoinValues.add(inputRecordValue);
|
||||
|
||||
// if we have a value in this field (and it's not the primary key), then it means we're updating part of the lock
|
||||
if(inputRecordValue != null && !joinOn.getRightField().equals(table.getPrimaryKeyField()))
|
||||
///////////////////////////////////////
|
||||
// we don't need the right-most join //
|
||||
///////////////////////////////////////
|
||||
if(!joinName.equals(rightMostJoin.getName()))
|
||||
{
|
||||
updatingAnyLockJoinFields = true;
|
||||
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
|
||||
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
|
||||
// also build up the query's sub-filters here (only adding them if they're unique). //
|
||||
// e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ListingHash<List<Serializable>, QRecord> inputRecordMapByJoinFields = new ListingHash<>();
|
||||
for(QRecord inputRecord : inputRecordPage)
|
||||
{
|
||||
List<Serializable> inputRecordJoinValues = new ArrayList<>();
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
QFieldType type = rightMostJoinTable.getField(joinOn.getRightField()).getType();
|
||||
Serializable inputRecordValue = ValueUtils.getValueAsFieldType(type, inputRecord.getValue(joinOn.getRightField()));
|
||||
inputRecordJoinValues.add(inputRecordValue);
|
||||
|
||||
subFilter.addCriteria(inputRecordValue == null
|
||||
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
|
||||
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
|
||||
}
|
||||
|
||||
subFilter.addCriteria(inputRecordValue == null
|
||||
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
|
||||
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// todo maybe, some version of? //
|
||||
//////////////////////////////////
|
||||
// if(action.equals(Action.UPDATE) && !updatingAnyLockJoinFields && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
|
||||
// {
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// // if this is a read-write lock, then if we have the record, it means we were able to read the record. //
|
||||
// // So if we're not updating the security field, then no error can come from it! //
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// continue;
|
||||
// }
|
||||
|
||||
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// only add this sub-filter if it's for a list of keys we haven't seen before //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
filter.addSubFilter(subFilter);
|
||||
}
|
||||
|
||||
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// execute the query for joined records - then put them in a map with keys corresponding to the join values //
|
||||
// e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
Map<List<Serializable>, QRecord> joinRecordMapByJoinFields = new HashMap<>();
|
||||
for(QRecord joinRecord : queryOutput.getRecords())
|
||||
{
|
||||
List<Serializable> joinRecordValues = new ArrayList<>();
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
|
||||
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
|
||||
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
{
|
||||
joinValue = joinRecord.getValue(joinOn.getLeftField());
|
||||
}
|
||||
joinRecordValues.add(joinValue);
|
||||
}
|
||||
|
||||
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// now for each input record, look for its joinRecord - if it isn't found, then this insert //
|
||||
// isn't allowed. if it is found, then validate its value matches this session's security keys //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(Map.Entry<List<Serializable>, List<QRecord>> entry : inputRecordMapByJoinFields.entrySet())
|
||||
{
|
||||
List<Serializable> inputRecordJoinValues = entry.getKey();
|
||||
List<QRecord> inputRecords = entry.getValue();
|
||||
if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
{
|
||||
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
|
||||
|
||||
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
|
||||
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
|
||||
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
|
||||
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
|
||||
{
|
||||
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// only add this sub-filter if it's for a list of keys we haven't seen before //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
filter.addSubFilter(subFilter);
|
||||
}
|
||||
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// execute the query for joined records - then put them in a map with keys corresponding to the join values //
|
||||
// e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
Map<List<Serializable>, QRecord> joinRecordMapByJoinFields = new HashMap<>();
|
||||
for(QRecord joinRecord : queryOutput.getRecords())
|
||||
{
|
||||
List<Serializable> joinRecordValues = new ArrayList<>();
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
List<QErrorMessage> recordErrors = validateRecordSecurityValue(table, recordSecurityLock, recordSecurityValue, field.getType(), action);
|
||||
if(CollectionUtils.nullSafeHasContents(recordErrors))
|
||||
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
|
||||
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
|
||||
{
|
||||
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).addAll(recordErrors, treePosition);
|
||||
joinValue = joinRecord.getValue(joinOn.getLeftField());
|
||||
}
|
||||
joinRecordValues.add(joinValue);
|
||||
}
|
||||
|
||||
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// now for each input record, look for its joinRecord - if it isn't found, then this insert //
|
||||
// isn't allowed. if it is found, then validate its value matches this session's security keys //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(Map.Entry<List<Serializable>, List<QRecord>> entry : inputRecordMapByJoinFields.entrySet())
|
||||
{
|
||||
List<Serializable> inputRecordJoinValues = entry.getKey();
|
||||
List<QRecord> inputRecords = entry.getValue();
|
||||
if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
{
|
||||
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
|
||||
|
||||
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
|
||||
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
|
||||
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
|
||||
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
|
||||
{
|
||||
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
|
||||
}
|
||||
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
validateRecordSecurityValue(table, inputRecord, recordSecurityLock, recordSecurityValue, field.getType(), action);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
else
|
||||
{
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
PermissionDeniedMessage error = new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found.");
|
||||
errorRecords.computeIfAbsent(inputRecord.getValue(primaryKeyField), (k) -> new RecordWithErrors(inputRecord)).add(error, treePosition);
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
inputRecord.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -363,81 +246,45 @@ public class ValidateRecordSecurityLockHelper
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** for tracking errors, we use primary keys. add "made up" ones to records
|
||||
** if needed (e.g., insert use-case).
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Map<Serializable, QRecord> makeUpPrimaryKeysIfNeeded(List<QRecord> records, QTableMetaData table)
|
||||
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table, Action action)
|
||||
{
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
Map<Serializable, QRecord> madeUpPrimaryKeys = new HashMap<>();
|
||||
Integer madeUpPrimaryKey = -1;
|
||||
for(QRecord record : records)
|
||||
List<RecordSecurityLock> recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
|
||||
List<RecordSecurityLock> locksToCheck = new ArrayList<>();
|
||||
|
||||
recordSecurityLocks = switch(action)
|
||||
{
|
||||
if(record.getValue(primaryKeyField) == null)
|
||||
{
|
||||
madeUpPrimaryKeys.put(madeUpPrimaryKey, record);
|
||||
record.setValue(primaryKeyField, madeUpPrimaryKey);
|
||||
madeUpPrimaryKey--;
|
||||
}
|
||||
}
|
||||
return madeUpPrimaryKeys;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a given table & action type, convert the table's record locks to a
|
||||
** MultiRecordSecurityLock, with only the appropriate lock-scopes being included
|
||||
** (e.g., read-locks for selects, write-locks for insert/update/delete).
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("checkstyle:Indentation")
|
||||
static MultiRecordSecurityLock getRecordSecurityLocks(QTableMetaData table, Action action)
|
||||
{
|
||||
List<RecordSecurityLock> allLocksOnTable = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
|
||||
MultiRecordSecurityLock locksOfType = switch(action)
|
||||
{
|
||||
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLockTree(allLocksOnTable);
|
||||
case SELECT -> RecordSecurityLockFilters.filterForReadLockTree(allLocksOnTable);
|
||||
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks);
|
||||
case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks);
|
||||
default -> throw (new IllegalArgumentException("Unsupported action: " + action));
|
||||
};
|
||||
|
||||
if(action.equals(Action.UPDATE))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// todo at some point this seemed right, but now it doesn't - needs work. //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// ////////////////////////////////////////////////////////
|
||||
// // when doing an update, convert all OR's to AND's... //
|
||||
// ////////////////////////////////////////////////////////
|
||||
// updateOperators(locksOfType, MultiRecordSecurityLock.BooleanOperator.AND);
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// if there are no locks, just return //
|
||||
////////////////////////////////////////
|
||||
if(locksOfType == null || CollectionUtils.nullSafeIsEmpty(locksOfType.getLocks()))
|
||||
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLocks))
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
return (locksOfType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** for a full multi-lock tree, set all of the boolean operators to the specified one.
|
||||
*******************************************************************************/
|
||||
private static void updateOperators(MultiRecordSecurityLock multiLock, MultiRecordSecurityLock.BooleanOperator operator)
|
||||
{
|
||||
multiLock.setOperator(operator);
|
||||
for(RecordSecurityLock childLock : multiLock.getLocks())
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// decide if any locks need checked - where one may not need checked if it has an all-access key, and the user has all-access //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(RecordSecurityLock recordSecurityLock : recordSecurityLocks)
|
||||
{
|
||||
if(childLock instanceof MultiRecordSecurityLock childMultiLock)
|
||||
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
|
||||
{
|
||||
updateOperators(childMultiLock, operator);
|
||||
LOG.trace("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock.");
|
||||
}
|
||||
else
|
||||
{
|
||||
locksToCheck.add(recordSecurityLock);
|
||||
}
|
||||
}
|
||||
|
||||
return (locksToCheck);
|
||||
}
|
||||
|
||||
|
||||
@ -445,7 +292,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static List<QErrorMessage> validateRecordSecurityValue(QTableMetaData table, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
|
||||
public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
|
||||
{
|
||||
if(recordSecurityValue == null)
|
||||
{
|
||||
@ -455,7 +302,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
|
||||
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel)));
|
||||
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -467,305 +314,15 @@ public class ValidateRecordSecurityLockHelper
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// avoid telling the user a value from a foreign record that they didn't pass in themselves. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record.")));
|
||||
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record."));
|
||||
}
|
||||
else
|
||||
{
|
||||
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
|
||||
return (List.of(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel())));
|
||||
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (Collections.emptyList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Class to track errors that we're associating with a record.
|
||||
**
|
||||
** More complex than it first seems to be needed, because as we're evaluating
|
||||
** locks, we might find some, but based on the boolean condition associated with
|
||||
** them, they might not actually be record-level errors.
|
||||
**
|
||||
** e.g., two locks with an OR relationship - as long as one passes, the record
|
||||
** should have no errors. And so-on through the tree of locks/multi-locks.
|
||||
**
|
||||
** Stores the errors in a tree of ErrorTreeNode objects.
|
||||
**
|
||||
** References into that tree are achieved via a List of Integer called "tree positions"
|
||||
** where each entry in the list denotes the index of the tree node at that level.
|
||||
**
|
||||
** e.g., given this tree:
|
||||
** <pre>
|
||||
** A B
|
||||
** / \ /|\
|
||||
** C D E F G
|
||||
** |
|
||||
** H
|
||||
** </pre>
|
||||
**
|
||||
** The positions of each node would be:
|
||||
** <pre>
|
||||
** A: [0]
|
||||
** B: [1]
|
||||
** C: [0,0]
|
||||
** D: [0,1]
|
||||
** E: [1,0]
|
||||
** F: [1,1]
|
||||
** G: [1,2]
|
||||
** H: [0,1,0]
|
||||
** </pre>
|
||||
*******************************************************************************/
|
||||
static class RecordWithErrors
|
||||
{
|
||||
private QRecord record;
|
||||
private ErrorTreeNode errorTree;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RecordWithErrors(QRecord record)
|
||||
{
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** add a list of errors, for a given list of tree positions
|
||||
*******************************************************************************/
|
||||
public void addAll(List<QErrorMessage> recordErrors, List<Integer> treePositions)
|
||||
{
|
||||
if(errorTree == null)
|
||||
{
|
||||
errorTree = new ErrorTreeNode();
|
||||
}
|
||||
|
||||
ErrorTreeNode node = errorTree;
|
||||
for(Integer treePosition : treePositions)
|
||||
{
|
||||
if(node.children == null)
|
||||
{
|
||||
node.children = new ArrayList<>(treePosition);
|
||||
}
|
||||
|
||||
while(treePosition >= node.children.size())
|
||||
{
|
||||
node.children.add(null);
|
||||
}
|
||||
|
||||
if(node.children.get(treePosition) == null)
|
||||
{
|
||||
node.children.set(treePosition, new ErrorTreeNode());
|
||||
}
|
||||
|
||||
node = node.children.get(treePosition);
|
||||
}
|
||||
|
||||
if(node.errors == null)
|
||||
{
|
||||
node.errors = new ArrayList<>();
|
||||
}
|
||||
node.errors.addAll(recordErrors);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** add a single error to a given tree-position
|
||||
*******************************************************************************/
|
||||
public void add(QErrorMessage error, List<Integer> treePositions)
|
||||
{
|
||||
addAll(List.of(error), treePositions);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** after the tree of errors has been built - walk a lock-tree (locksToCheck)
|
||||
** and resolve boolean operations, to get a final list of errors (possibly empty)
|
||||
** to put on the record.
|
||||
*******************************************************************************/
|
||||
public void propagateErrorsToRecord(MultiRecordSecurityLock locksToCheck)
|
||||
{
|
||||
List<QErrorMessage> errors = recursivePropagation(locksToCheck, new ArrayList<>());
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
errors.forEach(e -> record.addError(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** recursive implementation of the propagation method - e.g., walk tree applying
|
||||
** boolean logic.
|
||||
*******************************************************************************/
|
||||
private List<QErrorMessage> recursivePropagation(MultiRecordSecurityLock locksToCheck, List<Integer> treePositions)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// build a list of errors at this level (and deeper levels too) //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
List<QErrorMessage> errorsFromThisLevel = new ArrayList<>();
|
||||
|
||||
int i = 0;
|
||||
for(RecordSecurityLock lock : locksToCheck.getLocks())
|
||||
{
|
||||
List<QErrorMessage> errorsFromThisLock;
|
||||
|
||||
treePositions.add(i);
|
||||
if(lock instanceof MultiRecordSecurityLock childMultiLock)
|
||||
{
|
||||
errorsFromThisLock = recursivePropagation(childMultiLock, treePositions);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorsFromThisLock = getErrorsFromTree(treePositions);
|
||||
}
|
||||
|
||||
errorsFromThisLevel.addAll(errorsFromThisLock);
|
||||
|
||||
treePositions.remove(treePositions.size() - 1);
|
||||
i++;
|
||||
}
|
||||
|
||||
if(MultiRecordSecurityLock.BooleanOperator.AND.equals(locksToCheck.getOperator()))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// for an AND - if there were ANY errors, then return them. //
|
||||
//////////////////////////////////////////////////////////////
|
||||
if(!errorsFromThisLevel.isEmpty())
|
||||
{
|
||||
return (errorsFromThisLevel);
|
||||
}
|
||||
}
|
||||
else // OR
|
||||
{
|
||||
//////////////////////////////////////////////////////////
|
||||
// for an OR - only return if ALL conditions had errors //
|
||||
//////////////////////////////////////////////////////////
|
||||
if(errorsFromThisLevel.size() == locksToCheck.getLocks().size())
|
||||
{
|
||||
return (errorsFromThisLevel); // todo something smarter?
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// else - no errors - empty list //
|
||||
///////////////////////////////////
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QErrorMessage> getErrorsFromTree(List<Integer> treePositions)
|
||||
{
|
||||
ErrorTreeNode node = errorTree;
|
||||
|
||||
for(Integer treePosition : treePositions)
|
||||
{
|
||||
if(node.children == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if(treePosition >= node.children.size())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if(node.children.get(treePosition) == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
node = node.children.get(treePosition);
|
||||
}
|
||||
|
||||
if(node.errors == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return node.errors;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonUtils.toPrettyJson(this);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return "error in toString";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** tree node used by RecordWithErrors
|
||||
*******************************************************************************/
|
||||
static class ErrorTreeNode
|
||||
{
|
||||
private List<QErrorMessage> errors;
|
||||
private ArrayList<ErrorTreeNode> children;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonUtils.toPrettyJson(this);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return "error in toString";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for errors - only here for Jackson/toString
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<QErrorMessage> getErrors()
|
||||
{
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for children - only here for Jackson/toString
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ArrayList<ErrorTreeNode> getChildren()
|
||||
{
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -560,47 +560,20 @@ public class QPossibleValueTranslator
|
||||
*******************************************************************************/
|
||||
private void primePvsCache(String tableName, List<QPossibleValueSource> possibleValueSources, Collection<Serializable> values)
|
||||
{
|
||||
String idField = null;
|
||||
for(QPossibleValueSource possibleValueSource : possibleValueSources)
|
||||
{
|
||||
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
|
||||
String thisPvsIdField;
|
||||
if(StringUtils.hasContent(possibleValueSource.getOverrideIdField()))
|
||||
{
|
||||
thisPvsIdField = possibleValueSource.getOverrideIdField();
|
||||
}
|
||||
else
|
||||
{
|
||||
thisPvsIdField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
|
||||
}
|
||||
|
||||
if(idField == null)
|
||||
{
|
||||
idField = thisPvsIdField;
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// does this ever happen? maybe not... because, like, the list of values probably wouldn't make sense for //
|
||||
// more than one field in the table... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!idField.equals(thisPvsIdField))
|
||||
{
|
||||
for(QPossibleValueSource valueSource : possibleValueSources)
|
||||
{
|
||||
primePvsCache(tableName, List.of(valueSource), values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
String primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
|
||||
|
||||
for(List<Serializable> page : CollectionUtils.getPages(values, 1000))
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(idField, QCriteriaOperator.IN, page)));
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
|
||||
queryInput.setTransaction(getTransaction(tableName));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -645,7 +618,7 @@ public class QPossibleValueTranslator
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
Serializable pkeyValue = record.getValue(idField);
|
||||
Serializable pkeyValue = record.getValue(primaryKeyField);
|
||||
for(QPossibleValueSource possibleValueSource : possibleValueSources)
|
||||
{
|
||||
QPossibleValue<?> possibleValue = new QPossibleValue<>(pkeyValue, record.getRecordLabel());
|
||||
|
@ -28,6 +28,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -363,9 +364,7 @@ public class QValueFormatter
|
||||
}
|
||||
}
|
||||
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, QContext.getQInstance(), table, records, null);
|
||||
|
||||
setDisplayValuesInRecord(table, fieldMap, record, true);
|
||||
setDisplayValuesInRecord(fieldMap, record);
|
||||
record.setRecordLabel(formatRecordLabel(table, record));
|
||||
}
|
||||
}
|
||||
@ -375,49 +374,61 @@ public class QValueFormatter
|
||||
/*******************************************************************************
|
||||
** For a list of records, set their recordLabels and display values
|
||||
*******************************************************************************/
|
||||
public static void setDisplayValuesInRecords(QTableMetaData table, Map<String, QFieldMetaData> fields, List<QRecord> records)
|
||||
public static void setDisplayValuesInRecords(Collection<QFieldMetaData> fields, List<QRecord> records)
|
||||
{
|
||||
if(records == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(table != null)
|
||||
for(QRecord record : records)
|
||||
{
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, QContext.getQInstance(), table, records, null);
|
||||
setDisplayValuesInRecord(fields, record);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a list of records, set their recordLabels and display values
|
||||
*******************************************************************************/
|
||||
public static void setDisplayValuesInRecords(Map<String, QFieldMetaData> fields, List<QRecord> records)
|
||||
{
|
||||
if(records == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
setDisplayValuesInRecord(table, fields, record, true);
|
||||
setDisplayValuesInRecord(fields, record);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a single record, set its display values - public version of this.
|
||||
** For a list of records, set their display values
|
||||
*******************************************************************************/
|
||||
public static void setDisplayValuesInRecord(QTableMetaData table, Map<String, QFieldMetaData> fields, QRecord record)
|
||||
public static void setDisplayValuesInRecord(Collection<QFieldMetaData> fields, QRecord record)
|
||||
{
|
||||
setDisplayValuesInRecord(table, fields, record, false);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a single record, set its display values - where caller (meant to stay private)
|
||||
** can specify if they've already done fieldBehaviors (to avoid re-doing).
|
||||
*******************************************************************************/
|
||||
private static void setDisplayValuesInRecord(QTableMetaData table, Map<String, QFieldMetaData> fields, QRecord record, boolean alreadyAppliedFieldDisplayBehaviors)
|
||||
{
|
||||
if(!alreadyAppliedFieldDisplayBehaviors)
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
if(table != null)
|
||||
if(record.getDisplayValue(field.getName()) == null)
|
||||
{
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.FORMATTING, QContext.getQInstance(), table, List.of(record), null);
|
||||
String formattedValue = formatValue(field, record.getValue(field.getName()));
|
||||
record.setDisplayValue(field.getName(), formattedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a list of records, set their display values
|
||||
*******************************************************************************/
|
||||
public static void setDisplayValuesInRecord(Map<String, QFieldMetaData> fields, QRecord record)
|
||||
{
|
||||
for(Map.Entry<String, QFieldMetaData> entry : fields.entrySet())
|
||||
{
|
||||
String fieldName = entry.getKey();
|
||||
|
@ -275,19 +275,8 @@ public class SearchPossibleValueSourceAction
|
||||
|
||||
queryInput.setFilter(queryFilter);
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
String fieldName;
|
||||
if(StringUtils.hasContent(possibleValueSource.getOverrideIdField()))
|
||||
{
|
||||
fieldName = possibleValueSource.getOverrideIdField();
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldName = table.getPrimaryKeyField();
|
||||
}
|
||||
|
||||
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(fieldName)).toList();
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
|
||||
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
|
||||
output.setResults(qPossibleValues);
|
||||
|
||||
|
@ -27,7 +27,6 @@ import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldDisplayBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -45,8 +44,7 @@ public class ValueBehaviorApplier
|
||||
public enum Action
|
||||
{
|
||||
INSERT,
|
||||
UPDATE,
|
||||
FORMATTING
|
||||
UPDATE
|
||||
}
|
||||
|
||||
|
||||
@ -65,34 +63,7 @@ public class ValueBehaviorApplier
|
||||
{
|
||||
for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors()))
|
||||
{
|
||||
boolean applyBehavior = true;
|
||||
if(behaviorsToOmit != null && behaviorsToOmit.contains(fieldBehavior))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're given a set of behaviors to omit, and this behavior is in there, then skip //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
applyBehavior = false;
|
||||
}
|
||||
|
||||
if(Action.FORMATTING == action && !(fieldBehavior instanceof FieldDisplayBehavior<?>))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for the formatting action, do not apply the behavior unless it is a field-display-behavior //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
applyBehavior = false;
|
||||
}
|
||||
else if(Action.FORMATTING != action && fieldBehavior instanceof FieldDisplayBehavior<?>)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for non-formatting actions, do not apply the behavior IF it is a field-display-behavior //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
applyBehavior = false;
|
||||
}
|
||||
|
||||
if(applyBehavior)
|
||||
{
|
||||
fieldBehavior.apply(action, recordList, instance, table, field);
|
||||
}
|
||||
fieldBehavior.apply(action, recordList, instance, table, field, behaviorsToOmit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpRole;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
@ -112,7 +111,6 @@ public class QInstanceHelpContentManager
|
||||
String processName = nameValuePairs.get("process");
|
||||
String fieldName = nameValuePairs.get("field");
|
||||
String sectionName = nameValuePairs.get("section");
|
||||
String stepName = nameValuePairs.get("step");
|
||||
String widgetName = nameValuePairs.get("widget");
|
||||
String slotName = nameValuePairs.get("slot");
|
||||
|
||||
@ -147,11 +145,12 @@ public class QInstanceHelpContentManager
|
||||
}
|
||||
else if(StringUtils.hasContent(processName))
|
||||
{
|
||||
processHelpContentForProcess(key, processName, fieldName, stepName, roles, helpContent);
|
||||
processHelpContentForProcess(key, processName, fieldName, roles, helpContent);
|
||||
}
|
||||
else if(StringUtils.hasContent(widgetName))
|
||||
{
|
||||
processHelpContentForWidget(key, widgetName, slotName, roles, helpContent);
|
||||
processHelpContentForWidget(key, widgetName, slotName, helpContent);
|
||||
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -210,10 +209,6 @@ public class QInstanceHelpContentManager
|
||||
optionalSection.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Unrecognized key format for table help content", logPair("key", key));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -221,7 +216,7 @@ public class QInstanceHelpContentManager
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForProcess(String key, String processName, String fieldName, String stepName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
private static void processHelpContentForProcess(String key, String processName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
if(process == null)
|
||||
@ -250,30 +245,6 @@ public class QInstanceHelpContentManager
|
||||
optionalField.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else if(StringUtils.hasContent(stepName))
|
||||
{
|
||||
/////////////////////////////
|
||||
// handle a process screen //
|
||||
/////////////////////////////
|
||||
QFrontendStepMetaData frontendStep = process.getFrontendStep(stepName);
|
||||
|
||||
if(frontendStep == null)
|
||||
{
|
||||
LOG.info("Unrecognized process step in help content", logPair("key", key));
|
||||
}
|
||||
else if(helpContent != null)
|
||||
{
|
||||
frontendStep.withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
frontendStep.removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Unrecognized key format for process help content", logPair("key", key));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -281,7 +252,7 @@ public class QInstanceHelpContentManager
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForWidget(String key, String widgetName, String slotName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
private static void processHelpContentForWidget(String key, String widgetName, String slotName, QHelpContent helpContent)
|
||||
{
|
||||
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
|
||||
if(!StringUtils.hasContent(slotName))
|
||||
@ -294,14 +265,22 @@ public class QInstanceHelpContentManager
|
||||
}
|
||||
else
|
||||
{
|
||||
Map<String, QHelpContent> widgetHelpContent = widget.getHelpContent();
|
||||
if(widgetHelpContent == null)
|
||||
{
|
||||
widgetHelpContent = new HashMap<>();
|
||||
}
|
||||
|
||||
if(helpContent != null)
|
||||
{
|
||||
widget.withHelpContent(slotName, helpContent);
|
||||
widgetHelpContent.put(slotName, helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
widget.removeHelpContent(slotName, roles);
|
||||
widgetHelpContent.remove(slotName);
|
||||
}
|
||||
|
||||
widget.setHelpContent(widgetHelpContent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaD
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
@ -86,7 +85,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
|
||||
@ -625,11 +623,6 @@ public class QInstanceValidator
|
||||
supplementalTableMetaData.validate(qInstance, table, this);
|
||||
}
|
||||
|
||||
if(table.getShareableTableMetaData() != null)
|
||||
{
|
||||
table.getShareableTableMetaData().validate(qInstance, table, this);
|
||||
}
|
||||
|
||||
runPlugins(QTableMetaData.class, table, qInstance);
|
||||
});
|
||||
}
|
||||
@ -717,6 +710,7 @@ public class QInstanceValidator
|
||||
{
|
||||
String prefix = "Table " + table.getName() + " ";
|
||||
|
||||
RECORD_SECURITY_LOCKS_LOOP:
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
{
|
||||
if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)"))
|
||||
@ -724,129 +718,91 @@ public class QInstanceValidator
|
||||
continue;
|
||||
}
|
||||
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
|
||||
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
|
||||
{
|
||||
validateMultiRecordSecurityLock(qInstance, table, multiRecordSecurityLock, prefix);
|
||||
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
|
||||
}
|
||||
else
|
||||
|
||||
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
|
||||
|
||||
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
|
||||
|
||||
boolean hasAnyBadJoins = false;
|
||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
validateRecordSecurityLock(qInstance, table, recordSecurityLock, prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateMultiRecordSecurityLock(QInstance qInstance, QTableMetaData table, MultiRecordSecurityLock multiRecordSecurityLock, String prefix)
|
||||
{
|
||||
assertCondition(multiRecordSecurityLock.getOperator() != null, prefix + "has a MultiRecordSecurityLock that is missing an operator");
|
||||
|
||||
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
|
||||
{
|
||||
validateRecordSecurityLock(qInstance, table, lock, prefix);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateRecordSecurityLock(QInstance qInstance, QTableMetaData table, RecordSecurityLock recordSecurityLock, String prefix)
|
||||
{
|
||||
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
|
||||
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
|
||||
{
|
||||
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
|
||||
}
|
||||
|
||||
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
|
||||
|
||||
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
|
||||
|
||||
boolean hasAnyBadJoins = false;
|
||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
|
||||
{
|
||||
hasAnyBadJoins = true;
|
||||
}
|
||||
}
|
||||
|
||||
String fieldName = recordSecurityLock.getFieldName();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// don't bother trying to validate field names if we know we have a bad join. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
|
||||
{
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
|
||||
if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
|
||||
{
|
||||
String[] split = fieldName.split("\\.");
|
||||
String joinTableName = split[0];
|
||||
String joinFieldName = split[1];
|
||||
hasAnyBadJoins = true;
|
||||
}
|
||||
}
|
||||
|
||||
List<QueryJoin> joins = new ArrayList<>();
|
||||
String fieldName = recordSecurityLock.getFieldName();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - so - the join name chain is going to be like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
|
||||
// - securityFieldName = order.clientId //
|
||||
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
|
||||
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
|
||||
// and step (via tmpTable variable) back to the securityField //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
|
||||
Collections.reverse(joinNameChain);
|
||||
|
||||
QTableMetaData tmpTable = table;
|
||||
|
||||
for(String joinName : joinNameChain)
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// don't bother trying to validate field names if we know we have a bad join. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
|
||||
{
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
|
||||
{
|
||||
QJoinMetaData join = qInstance.getJoin(joinName);
|
||||
if(join == null)
|
||||
List<QueryJoin> joins = new ArrayList<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - so - the join name chain is going to be like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
|
||||
// - securityFieldName = order.clientId //
|
||||
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
|
||||
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
|
||||
// and step (via tmpTable variable) back to the securityField //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
|
||||
Collections.reverse(joinNameChain);
|
||||
|
||||
QTableMetaData tmpTable = table;
|
||||
|
||||
for(String joinName : joinNameChain)
|
||||
{
|
||||
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
|
||||
return;
|
||||
QJoinMetaData join = qInstance.getJoin(joinName);
|
||||
if(join == null)
|
||||
{
|
||||
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
|
||||
continue RECORD_SECURITY_LOCKS_LOOP;
|
||||
}
|
||||
|
||||
if(join.getLeftTable().equals(tmpTable.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join));
|
||||
tmpTable = qInstance.getTable(join.getRightTable());
|
||||
}
|
||||
else if(join.getRightTable().equals(tmpTable.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join.flip()));
|
||||
tmpTable = qInstance.getTable(join.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
|
||||
continue RECORD_SECURITY_LOCKS_LOOP;
|
||||
}
|
||||
}
|
||||
|
||||
if(join.getLeftTable().equals(tmpTable.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join));
|
||||
tmpTable = qInstance.getTable(join.getRightTable());
|
||||
}
|
||||
else if(join.getRightTable().equals(tmpTable.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join.flip()));
|
||||
tmpTable = qInstance.getTable(join.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
|
||||
return;
|
||||
}
|
||||
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
|
||||
assertCondition(Objects.equals(tmpTable.getName(), joinTableName), prefix + "has a joinNameChain doesn't end in the expected table [" + joinTableName + "] (was: " + tmpTable.getName() + ")");
|
||||
|
||||
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " does not look like a join (does not have a dot), but a joinNameChain was given."))
|
||||
else
|
||||
{
|
||||
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
if(assertCondition(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " does not look like a join (does not have a dot), but a joinNameChain was given."))
|
||||
{
|
||||
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
|
||||
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -854,7 +810,7 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private <T extends FieldBehavior<T>> void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
|
||||
private void validateTableField(QInstance qInstance, String tableName, String fieldName, QTableMetaData table, QFieldMetaData field)
|
||||
{
|
||||
assertCondition(Objects.equals(fieldName, field.getName()),
|
||||
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
|
||||
@ -867,32 +823,12 @@ public class QInstanceValidator
|
||||
|
||||
String prefix = "Field " + fieldName + " in table " + tableName + " ";
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// validate things we know about field behaviors //
|
||||
///////////////////////////////////////////////////
|
||||
ValueTooLongBehavior behavior = field.getBehaviorOrDefault(qInstance, ValueTooLongBehavior.class);
|
||||
if(behavior != null && !behavior.equals(ValueTooLongBehavior.PASS_THROUGH))
|
||||
{
|
||||
assertCondition(field.getMaxLength() != null, prefix + "specifies a ValueTooLongBehavior, but not a maxLength.");
|
||||
}
|
||||
|
||||
Set<Class<FieldBehavior<T>>> usedFieldBehaviorTypes = new HashSet<>();
|
||||
if(field.getBehaviors() != null)
|
||||
{
|
||||
for(FieldBehavior<?> fieldBehavior : field.getBehaviors())
|
||||
{
|
||||
Class<FieldBehavior<T>> behaviorClass = (Class<FieldBehavior<T>>) fieldBehavior.getClass();
|
||||
|
||||
errors.addAll(fieldBehavior.validateBehaviorConfiguration(table, field));
|
||||
|
||||
if(!fieldBehavior.allowMultipleBehaviorsOfThisType())
|
||||
{
|
||||
assertCondition(!usedFieldBehaviorTypes.contains(behaviorClass), prefix + "has more than 1 fieldBehavior of type " + behaviorClass.getSimpleName() + ", which is not allowed for this type");
|
||||
}
|
||||
usedFieldBehaviorTypes.add(behaviorClass);
|
||||
}
|
||||
}
|
||||
|
||||
if(field.getMaxLength() != null)
|
||||
{
|
||||
assertCondition(field.getMaxLength() > 0, prefix + "has an invalid maxLength (" + field.getMaxLength() + ") - must be greater than 0.");
|
||||
@ -1426,17 +1362,12 @@ public class QInstanceValidator
|
||||
///////////////////////////////////
|
||||
// validate steps in the process //
|
||||
///////////////////////////////////
|
||||
Set<String> usedStepNames = new HashSet<>();
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(process.getStepList()), "At least 1 step must be defined in process " + processName + "."))
|
||||
{
|
||||
int index = 0;
|
||||
for(QStepMetaData step : process.getStepList())
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName))
|
||||
{
|
||||
assertCondition(!usedStepNames.contains(step.getName()), "Duplicate step name [" + step.getName() + "] in process " + processName);
|
||||
usedStepNames.add(step.getName());
|
||||
}
|
||||
assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName);
|
||||
index++;
|
||||
|
||||
////////////////////////////////////////////
|
||||
@ -1513,7 +1444,7 @@ public class QInstanceValidator
|
||||
private void validateScheduleMetaData(QScheduleMetaData schedule, QInstance qInstance, String prefix)
|
||||
{
|
||||
boolean isRepeat = schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null;
|
||||
boolean isCron = StringUtils.hasContent(schedule.getCronExpression());
|
||||
boolean isCron = StringUtils.hasContent(schedule.getCronExpression());
|
||||
assertCondition(isRepeat || isCron, prefix + " either repeatMillis or repeatSeconds or cronExpression must be set");
|
||||
assertCondition(!(isRepeat && isCron), prefix + " both a repeat time and cronExpression may not be set");
|
||||
|
||||
@ -1533,8 +1464,8 @@ public class QInstanceValidator
|
||||
|
||||
if(assertCondition(StringUtils.hasContent(schedule.getCronTimeZoneId()), prefix + " a cron schedule must specify a cronTimeZoneId"))
|
||||
{
|
||||
String[] availableIDs = TimeZone.getAvailableIDs();
|
||||
Optional<String> first = Arrays.stream(availableIDs).filter(id -> id.equals(schedule.getCronTimeZoneId())).findFirst();
|
||||
String[] availableIDs = TimeZone.getAvailableIDs();
|
||||
Optional<String> first = Arrays.stream(availableIDs).filter(id -> id.equals(schedule.getCronTimeZoneId())).findFirst();
|
||||
assertCondition(first.isPresent(), prefix + " unrecognized cronTimeZoneId: " + schedule.getCronTimeZoneId());
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ public class LogPair
|
||||
}
|
||||
else if(value instanceof LogPair[] subLogPairs)
|
||||
{
|
||||
String subLogPairsString = Arrays.stream(subLogPairs).filter(Objects::nonNull).map(LogPair::toString).collect(Collectors.joining(","));
|
||||
String subLogPairsString = Arrays.stream(subLogPairs).map(LogPair::toString).collect(Collectors.joining(","));
|
||||
valueString = '{' + subLogPairsString + '}';
|
||||
}
|
||||
else if(value instanceof UnsafeSupplier<?, ?> us)
|
||||
|
@ -22,7 +22,6 @@
|
||||
package com.kingsrook.qqq.backend.core.logging;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -148,28 +147,6 @@ public class QLogger
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public <T extends Throwable> T warnAndThrow(T t, LogPair... logPairs) throws T
|
||||
{
|
||||
warn(t.getMessage(), t, logPairs);
|
||||
throw (t);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public <T extends Throwable> T errorAndThrow(T t, LogPair... logPairs) throws T
|
||||
{
|
||||
error(t.getMessage(), t, logPairs);
|
||||
throw (t);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -618,10 +595,7 @@ public class QLogger
|
||||
{
|
||||
user = session.getUser().getIdReference();
|
||||
}
|
||||
|
||||
LogPair variantsLogPair = getVariantsLogPair(session);
|
||||
|
||||
sessionLogPair = logPair("session", logPair("id", session.getUuid()), logPair("user", user), variantsLogPair);
|
||||
sessionLogPair = logPair("session", logPair("id", session.getUuid()), logPair("user", user));
|
||||
}
|
||||
|
||||
try
|
||||
@ -641,38 +615,6 @@ public class QLogger
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static LogPair getVariantsLogPair(QSession session)
|
||||
{
|
||||
LogPair variantsLogPair = null;
|
||||
try
|
||||
{
|
||||
if(session.getBackendVariants() != null)
|
||||
{
|
||||
LogPair[] variants = new LogPair[session.getBackendVariants().size()];
|
||||
|
||||
int i = 0;
|
||||
for(Map.Entry<String, Serializable> entry : session.getBackendVariants().entrySet())
|
||||
{
|
||||
variants[i] = new LogPair(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
variantsLogPair = new LogPair("variants", variants);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
////////////////
|
||||
// leave null //
|
||||
////////////////
|
||||
}
|
||||
return variantsLogPair;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -26,7 +26,6 @@ import java.util.UUID;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.NonPersistedAsyncJobCallback;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
@ -140,7 +139,7 @@ public class AbstractActionInput
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// don't return null here (too easy to NPE). instead, if someone wants one of these, create one and give it to them. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
asyncJobCallback = new NonPersistedAsyncJobCallback(UUID.randomUUID(), new AsyncJobStatus().withJobName(getClass().getSimpleName()));
|
||||
asyncJobCallback = new AsyncJobCallback(UUID.randomUUID(), new AsyncJobStatus());
|
||||
}
|
||||
return asyncJobCallback;
|
||||
}
|
||||
|
@ -1,95 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class Attachment
|
||||
{
|
||||
private byte[] contents;
|
||||
private String name;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contents
|
||||
*******************************************************************************/
|
||||
public byte[] getContents()
|
||||
{
|
||||
return (this.contents);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for contents
|
||||
*******************************************************************************/
|
||||
public void setContents(byte[] contents)
|
||||
{
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for contents
|
||||
*******************************************************************************/
|
||||
public Attachment withContents(byte[] contents)
|
||||
{
|
||||
this.contents = contents;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public Attachment withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class Content
|
||||
{
|
||||
private String body;
|
||||
private ContentRole contentRole;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for body
|
||||
*******************************************************************************/
|
||||
public String getBody()
|
||||
{
|
||||
return (this.body);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for body
|
||||
*******************************************************************************/
|
||||
public void setBody(String body)
|
||||
{
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for body
|
||||
*******************************************************************************/
|
||||
public Content withBody(String body)
|
||||
{
|
||||
this.body = body;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contentRole
|
||||
*******************************************************************************/
|
||||
public ContentRole getContentRole()
|
||||
{
|
||||
return (this.contentRole);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for contentRole
|
||||
*******************************************************************************/
|
||||
public void setContentRole(ContentRole contentRole)
|
||||
{
|
||||
this.contentRole = contentRole;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for contentRole
|
||||
*******************************************************************************/
|
||||
public Content withContentRole(ContentRole contentRole)
|
||||
{
|
||||
this.contentRole = contentRole;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface ContentRole
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
enum Default implements ContentRole
|
||||
{
|
||||
DEFAULT
|
||||
}
|
||||
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class MultiParty extends Party
|
||||
{
|
||||
private List<Party> partyList;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for partyList
|
||||
*******************************************************************************/
|
||||
public List<Party> getPartyList()
|
||||
{
|
||||
return (this.partyList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for partyList
|
||||
*******************************************************************************/
|
||||
public void setPartyList(List<Party> partyList)
|
||||
{
|
||||
this.partyList = partyList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for partyList
|
||||
*******************************************************************************/
|
||||
public MultiParty withPartyList(List<Party> partyList)
|
||||
{
|
||||
this.partyList = partyList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public MultiParty withParty(Party party)
|
||||
{
|
||||
addParty(party);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addParty(Party party)
|
||||
{
|
||||
if(this.partyList == null)
|
||||
{
|
||||
this.partyList = new ArrayList<>();
|
||||
}
|
||||
this.partyList.add(party);
|
||||
}
|
||||
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class Party
|
||||
{
|
||||
private String label;
|
||||
private String address;
|
||||
private PartyRole role;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (this.label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for label
|
||||
*******************************************************************************/
|
||||
public void setLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for label
|
||||
*******************************************************************************/
|
||||
public Party withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for address
|
||||
*******************************************************************************/
|
||||
public String getAddress()
|
||||
{
|
||||
return (this.address);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for address
|
||||
*******************************************************************************/
|
||||
public void setAddress(String address)
|
||||
{
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for address
|
||||
*******************************************************************************/
|
||||
public Party withAddress(String address)
|
||||
{
|
||||
this.address = address;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for role
|
||||
*******************************************************************************/
|
||||
public PartyRole getRole()
|
||||
{
|
||||
return (this.role);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for role
|
||||
*******************************************************************************/
|
||||
public void setRole(PartyRole role)
|
||||
{
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for role
|
||||
*******************************************************************************/
|
||||
public Party withRole(PartyRole role)
|
||||
{
|
||||
this.role = role;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface PartyRole
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
enum Default implements PartyRole
|
||||
{
|
||||
DEFAULT
|
||||
}
|
||||
|
||||
}
|
@ -1,278 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class SendMessageInput extends AbstractActionInput
|
||||
{
|
||||
private String messagingProviderName;
|
||||
private Party to;
|
||||
private Party from;
|
||||
private String subject;
|
||||
private List<Content> contentList;
|
||||
private List<Attachment> attachmentList;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for to
|
||||
*******************************************************************************/
|
||||
public Party getTo()
|
||||
{
|
||||
return (this.to);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for to
|
||||
*******************************************************************************/
|
||||
public void setTo(Party to)
|
||||
{
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for to
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withTo(Party to)
|
||||
{
|
||||
this.to = to;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for from
|
||||
*******************************************************************************/
|
||||
public Party getFrom()
|
||||
{
|
||||
return (this.from);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for from
|
||||
*******************************************************************************/
|
||||
public void setFrom(Party from)
|
||||
{
|
||||
this.from = from;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for from
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withFrom(Party from)
|
||||
{
|
||||
this.from = from;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for subject
|
||||
*******************************************************************************/
|
||||
public String getSubject()
|
||||
{
|
||||
return (this.subject);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for subject
|
||||
*******************************************************************************/
|
||||
public void setSubject(String subject)
|
||||
{
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for subject
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withSubject(String subject)
|
||||
{
|
||||
this.subject = subject;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for contentList
|
||||
*******************************************************************************/
|
||||
public List<Content> getContentList()
|
||||
{
|
||||
return (this.contentList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for contentList
|
||||
*******************************************************************************/
|
||||
public void setContentList(List<Content> contentList)
|
||||
{
|
||||
this.contentList = contentList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for contentList
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withContentList(List<Content> contentList)
|
||||
{
|
||||
this.contentList = contentList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for attachmentList
|
||||
*******************************************************************************/
|
||||
public List<Attachment> getAttachmentList()
|
||||
{
|
||||
return (this.attachmentList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for attachmentList
|
||||
*******************************************************************************/
|
||||
public void setAttachmentList(List<Attachment> attachmentList)
|
||||
{
|
||||
this.attachmentList = attachmentList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for attachmentList
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withAttachmentList(List<Attachment> attachmentList)
|
||||
{
|
||||
this.attachmentList = attachmentList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withContent(Content content)
|
||||
{
|
||||
addContent(content);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addContent(Content content)
|
||||
{
|
||||
if(this.contentList == null)
|
||||
{
|
||||
this.contentList = new ArrayList<>();
|
||||
}
|
||||
this.contentList.add(content);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withAttachment(Attachment attachment)
|
||||
{
|
||||
addAttachment(attachment);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addAttachment(Attachment attachment)
|
||||
{
|
||||
if(this.attachmentList == null)
|
||||
{
|
||||
this.attachmentList = new ArrayList<>();
|
||||
}
|
||||
this.attachmentList.add(attachment);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for messagingProviderName
|
||||
*******************************************************************************/
|
||||
public String getMessagingProviderName()
|
||||
{
|
||||
return (this.messagingProviderName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for messagingProviderName
|
||||
*******************************************************************************/
|
||||
public void setMessagingProviderName(String messagingProviderName)
|
||||
{
|
||||
this.messagingProviderName = messagingProviderName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for messagingProviderName
|
||||
*******************************************************************************/
|
||||
public SendMessageInput withMessagingProviderName(String messagingProviderName)
|
||||
{
|
||||
this.messagingProviderName = messagingProviderName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class SendMessageOutput extends AbstractActionOutput
|
||||
{
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging.email;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.messaging.ContentRole;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum EmailContentRole implements ContentRole
|
||||
{
|
||||
TEXT,
|
||||
HTML
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.messaging.email;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.messaging.PartyRole;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum EmailPartyRole implements PartyRole
|
||||
{
|
||||
TO,
|
||||
CC,
|
||||
BCC,
|
||||
FROM,
|
||||
REPLY_TO
|
||||
}
|
@ -30,7 +30,6 @@ import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.NonPersistedAsyncJobCallback;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
@ -451,7 +450,7 @@ public class RunBackendStepInput extends AbstractActionInput
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// avoid NPE in case we didn't have one of these! create a new one... //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
asyncJobCallback = new NonPersistedAsyncJobCallback(UUID.randomUUID(), new AsyncJobStatus().withJobName(processName + "." + stepName));
|
||||
asyncJobCallback = new AsyncJobCallback(UUID.randomUUID(), new AsyncJobStatus());
|
||||
}
|
||||
return (asyncJobCallback);
|
||||
}
|
||||
|
@ -27,14 +27,10 @@ import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -44,14 +40,9 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
*******************************************************************************/
|
||||
public class RunBackendStepOutput extends AbstractActionOutput implements Serializable
|
||||
{
|
||||
private String processName;
|
||||
|
||||
private ProcessState processState;
|
||||
private Exception exception; // todo - make optional
|
||||
|
||||
private String overrideLastStepName;
|
||||
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
|
||||
|
||||
private List<AuditInput> auditInputList = new ArrayList<>();
|
||||
|
||||
|
||||
@ -87,7 +78,6 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
|
||||
public void seedFromRequest(RunBackendStepInput runBackendStepInput)
|
||||
{
|
||||
this.processState = runBackendStepInput.getProcessState();
|
||||
this.processName = runBackendStepInput.getProcessName();
|
||||
}
|
||||
|
||||
|
||||
@ -322,122 +312,4 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
|
||||
auditInput.addAuditSingleInput(auditSingleInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for overrideLastStepName
|
||||
*******************************************************************************/
|
||||
public String getOverrideLastStepName()
|
||||
{
|
||||
return (this.overrideLastStepName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for overrideLastStepName
|
||||
*******************************************************************************/
|
||||
public void setOverrideLastStepName(String overrideLastStepName)
|
||||
{
|
||||
this.overrideLastStepName = overrideLastStepName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for overrideLastStepName
|
||||
*******************************************************************************/
|
||||
public RunBackendStepOutput withOverrideLastStepName(String overrideLastStepName)
|
||||
{
|
||||
this.overrideLastStepName = overrideLastStepName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void updateStepList(List<String> stepList)
|
||||
{
|
||||
getProcessState().setStepList(stepList);
|
||||
|
||||
if(processName == null)
|
||||
{
|
||||
throw (new QRuntimeException("ProcessName was not set in this object, therefore updateStepList cannot complete successfully. Try to manually call setProcessName as a work around."));
|
||||
}
|
||||
|
||||
QProcessMetaData processMetaData = QContext.getQInstance().getProcess(processName);
|
||||
|
||||
ArrayList<QFrontendStepMetaData> updatedFrontendStepList = new ArrayList<>(stepList.stream()
|
||||
.map(name -> processMetaData.getStep(name))
|
||||
.filter(step -> step instanceof QFrontendStepMetaData)
|
||||
.map(step -> (QFrontendStepMetaData) step)
|
||||
.toList());
|
||||
|
||||
setUpdatedFrontendStepList(updatedFrontendStepList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for processName
|
||||
*******************************************************************************/
|
||||
public String getProcessName()
|
||||
{
|
||||
return (this.processName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for processName
|
||||
*******************************************************************************/
|
||||
public void setProcessName(String processName)
|
||||
{
|
||||
this.processName = processName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for processName
|
||||
*******************************************************************************/
|
||||
public RunBackendStepOutput withProcessName(String processName)
|
||||
{
|
||||
this.processName = processName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
|
||||
{
|
||||
return (this.updatedFrontendStepList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public RunBackendStepOutput withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -46,8 +45,6 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
|
||||
private String processUUID;
|
||||
private Optional<Exception> exception = Optional.empty();
|
||||
|
||||
private List<QFrontendStepMetaData> updatedFrontendStepList = null;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -330,36 +327,4 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
|
||||
{
|
||||
return exception;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public List<QFrontendStepMetaData> getUpdatedFrontendStepList()
|
||||
{
|
||||
return (this.updatedFrontendStepList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public void setUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updatedFrontendStepList
|
||||
*******************************************************************************/
|
||||
public RunProcessOutput withUpdatedFrontendStepList(List<QFrontendStepMetaData> updatedFrontendStepList)
|
||||
{
|
||||
this.updatedFrontendStepList = updatedFrontendStepList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.reporting;
|
||||
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
@ -36,8 +37,9 @@ public class ExportInput extends AbstractTableActionInput
|
||||
private Integer limit;
|
||||
private List<String> fieldNames;
|
||||
|
||||
private ReportDestination reportDestination;
|
||||
|
||||
private String filename;
|
||||
private ReportFormat reportFormat;
|
||||
private OutputStream reportOutputStream;
|
||||
private String titleRow;
|
||||
private boolean includeHeaderRow = true;
|
||||
|
||||
@ -118,6 +120,71 @@ public class ExportInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filename
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getFilename()
|
||||
{
|
||||
return filename;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for filename
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setFilename(String filename)
|
||||
{
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportFormat
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ReportFormat getReportFormat()
|
||||
{
|
||||
return reportFormat;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportFormat
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setReportFormat(ReportFormat reportFormat)
|
||||
{
|
||||
this.reportFormat = reportFormat;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportOutputStream
|
||||
**
|
||||
*******************************************************************************/
|
||||
public OutputStream getReportOutputStream()
|
||||
{
|
||||
return reportOutputStream;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportOutputStream
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setReportOutputStream(OutputStream reportOutputStream)
|
||||
{
|
||||
this.reportOutputStream = reportOutputStream;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -159,36 +226,4 @@ public class ExportInput extends AbstractTableActionInput
|
||||
this.includeHeaderRow = includeHeaderRow;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportDestination
|
||||
*******************************************************************************/
|
||||
public ReportDestination getReportDestination()
|
||||
{
|
||||
return (this.reportDestination);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportDestination
|
||||
*******************************************************************************/
|
||||
public void setReportDestination(ReportDestination reportDestination)
|
||||
{
|
||||
this.reportDestination = reportDestination;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for reportDestination
|
||||
*******************************************************************************/
|
||||
public ExportInput withReportDestination(ReportDestination reportDestination)
|
||||
{
|
||||
this.reportDestination = reportDestination;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,131 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.reporting;
|
||||
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Member of report & export Inputs, that wraps details about the destination of
|
||||
** where & how the report (or export) is being written.
|
||||
*******************************************************************************/
|
||||
public class ReportDestination
|
||||
{
|
||||
private String filename;
|
||||
private ReportFormat reportFormat;
|
||||
private OutputStream reportOutputStream;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filename
|
||||
*******************************************************************************/
|
||||
public String getFilename()
|
||||
{
|
||||
return (this.filename);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for filename
|
||||
*******************************************************************************/
|
||||
public void setFilename(String filename)
|
||||
{
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for filename
|
||||
*******************************************************************************/
|
||||
public ReportDestination withFilename(String filename)
|
||||
{
|
||||
this.filename = filename;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportFormat
|
||||
*******************************************************************************/
|
||||
public ReportFormat getReportFormat()
|
||||
{
|
||||
return (this.reportFormat);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportFormat
|
||||
*******************************************************************************/
|
||||
public void setReportFormat(ReportFormat reportFormat)
|
||||
{
|
||||
this.reportFormat = reportFormat;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for reportFormat
|
||||
*******************************************************************************/
|
||||
public ReportDestination withReportFormat(ReportFormat reportFormat)
|
||||
{
|
||||
this.reportFormat = reportFormat;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportOutputStream
|
||||
*******************************************************************************/
|
||||
public OutputStream getReportOutputStream()
|
||||
{
|
||||
return (this.reportOutputStream);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportOutputStream
|
||||
*******************************************************************************/
|
||||
public void setReportOutputStream(OutputStream reportOutputStream)
|
||||
{
|
||||
this.reportOutputStream = reportOutputStream;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for reportOutputStream
|
||||
*******************************************************************************/
|
||||
public ReportDestination withReportOutputStream(OutputStream reportOutputStream)
|
||||
{
|
||||
this.reportOutputStream = reportOutputStream;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -25,13 +25,13 @@ package com.kingsrook.qqq.backend.core.model.actions.reporting;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Supplier;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.CsvExportStreamer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExcelExportStreamer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.JsonExportStreamer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ListOfMapsExportStreamer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.excel.poi.ExcelPoiBasedStreamingExportStreamer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.poi.ss.SpreadsheetVersion;
|
||||
import org.dhatim.fastexcel.Worksheet;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -39,24 +39,15 @@ import org.apache.poi.ss.SpreadsheetVersion;
|
||||
*******************************************************************************/
|
||||
public enum ReportFormat
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// if we need to fall back to Fastexcel, this was its version of this. //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelFastexcelExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx", true, false, true),
|
||||
|
||||
XLSX(SpreadsheetVersion.EXCEL2007.getMaxRows(), SpreadsheetVersion.EXCEL2007.getMaxColumns(), ExcelPoiBasedStreamingExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx", true, true, true),
|
||||
JSON(null, null, JsonExportStreamer::new, "application/json", "json", false, false, true),
|
||||
CSV(null, null, CsvExportStreamer::new, "text/csv", "csv", false, false, false),
|
||||
LIST_OF_MAPS(null, null, ListOfMapsExportStreamer::new, null, null, false, false, true);
|
||||
XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
||||
JSON(null, null, JsonExportStreamer::new, "application/json"),
|
||||
CSV(null, null, CsvExportStreamer::new, "text/csv"),
|
||||
LIST_OF_MAPS(null, null, ListOfMapsExportStreamer::new, null);
|
||||
|
||||
|
||||
private final Integer maxRows;
|
||||
private final Integer maxCols;
|
||||
private final String mimeType;
|
||||
private final String extension;
|
||||
private final boolean isBinary;
|
||||
private final boolean supportsNativePivotTables;
|
||||
private final boolean supportsMultipleViews;
|
||||
|
||||
private final Supplier<? extends ExportStreamerInterface> streamerConstructor;
|
||||
|
||||
@ -65,16 +56,12 @@ public enum ReportFormat
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
ReportFormat(Integer maxRows, Integer maxCols, Supplier<? extends ExportStreamerInterface> streamerConstructor, String mimeType, String extension, boolean isBinary, boolean supportsNativePivotTables, boolean supportsMultipleViews)
|
||||
ReportFormat(Integer maxRows, Integer maxCols, Supplier<? extends ExportStreamerInterface> streamerConstructor, String mimeType)
|
||||
{
|
||||
this.maxRows = maxRows;
|
||||
this.maxCols = maxCols;
|
||||
this.mimeType = mimeType;
|
||||
this.streamerConstructor = streamerConstructor;
|
||||
this.extension = extension;
|
||||
this.isBinary = isBinary;
|
||||
this.supportsNativePivotTables = supportsNativePivotTables;
|
||||
this.supportsMultipleViews = supportsMultipleViews;
|
||||
}
|
||||
|
||||
|
||||
@ -141,48 +128,4 @@ public enum ReportFormat
|
||||
{
|
||||
return (streamerConstructor.get());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for extension
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getExtension()
|
||||
{
|
||||
return extension;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isBinary
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean getIsBinary()
|
||||
{
|
||||
return isBinary;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for supportsNativePivotTables
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean getSupportsNativePivotTables()
|
||||
{
|
||||
return supportsNativePivotTables;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for supportsMultipleViews
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean getSupportsMultipleViews()
|
||||
{
|
||||
return supportsMultipleViews;
|
||||
}
|
||||
}
|
||||
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 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.core.model.actions.reporting;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** sub-set of ReportFormats to expose as possible-values in-apps
|
||||
*******************************************************************************/
|
||||
public enum ReportFormatPossibleValueEnum implements PossibleValueEnum<String>
|
||||
{
|
||||
XLSX,
|
||||
CSV,
|
||||
JSON;
|
||||
|
||||
public static final String NAME = "reportFormat";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getPossibleValueId()
|
||||
{
|
||||
return name();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getPossibleValueLabel()
|
||||
{
|
||||
return name();
|
||||
}
|
||||
}
|
@ -22,28 +22,24 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.reporting;
|
||||
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Input for a Report action
|
||||
** Input for an Export action
|
||||
*******************************************************************************/
|
||||
public class ReportInput extends AbstractTableActionInput
|
||||
{
|
||||
private String reportName;
|
||||
private QReportMetaData reportMetaData;
|
||||
|
||||
private String reportName;
|
||||
private Map<String, Serializable> inputValues;
|
||||
|
||||
private ReportDestination reportDestination;
|
||||
|
||||
private Supplier<? extends ExportStreamerInterface> overrideExportStreamerSupplier;
|
||||
private String filename;
|
||||
private ReportFormat reportFormat;
|
||||
private OutputStream reportOutputStream;
|
||||
|
||||
|
||||
|
||||
@ -115,97 +111,66 @@ public class ReportInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportDestination
|
||||
*******************************************************************************/
|
||||
public ReportDestination getReportDestination()
|
||||
{
|
||||
return (this.reportDestination);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportDestination
|
||||
*******************************************************************************/
|
||||
public void setReportDestination(ReportDestination reportDestination)
|
||||
{
|
||||
this.reportDestination = reportDestination;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for reportDestination
|
||||
*******************************************************************************/
|
||||
public ReportInput withReportDestination(ReportDestination reportDestination)
|
||||
{
|
||||
this.reportDestination = reportDestination;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportMetaData
|
||||
*******************************************************************************/
|
||||
public QReportMetaData getReportMetaData()
|
||||
{
|
||||
return (this.reportMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportMetaData
|
||||
*******************************************************************************/
|
||||
public void setReportMetaData(QReportMetaData reportMetaData)
|
||||
{
|
||||
this.reportMetaData = reportMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for reportMetaData
|
||||
*******************************************************************************/
|
||||
public ReportInput withReportMetaData(QReportMetaData reportMetaData)
|
||||
{
|
||||
this.reportMetaData = reportMetaData;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for overrideExportStreamerSupplier
|
||||
** Getter for filename
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Supplier<? extends ExportStreamerInterface> getOverrideExportStreamerSupplier()
|
||||
public String getFilename()
|
||||
{
|
||||
return overrideExportStreamerSupplier;
|
||||
return filename;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for overrideExportStreamerSupplier
|
||||
** Setter for filename
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setOverrideExportStreamerSupplier(Supplier<? extends ExportStreamerInterface> overrideExportStreamerSupplier)
|
||||
public void setFilename(String filename)
|
||||
{
|
||||
this.overrideExportStreamerSupplier = overrideExportStreamerSupplier;
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for overrideExportStreamerSupplier
|
||||
** Getter for reportFormat
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ReportInput withOverrideExportStreamerSupplier(Supplier<? extends ExportStreamerInterface> overrideExportStreamerSupplier)
|
||||
public ReportFormat getReportFormat()
|
||||
{
|
||||
this.overrideExportStreamerSupplier = overrideExportStreamerSupplier;
|
||||
return (this);
|
||||
return reportFormat;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportFormat
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setReportFormat(ReportFormat reportFormat)
|
||||
{
|
||||
this.reportFormat = reportFormat;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportOutputStream
|
||||
**
|
||||
*******************************************************************************/
|
||||
public OutputStream getReportOutputStream()
|
||||
{
|
||||
return reportOutputStream;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportOutputStream
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setReportOutputStream(OutputStream reportOutputStream)
|
||||
{
|
||||
this.reportOutputStream = reportOutputStream;
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +0,0 @@
|
||||
/*
|
||||
* 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.core.model.actions.reporting;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Output for a Report action
|
||||
*******************************************************************************/
|
||||
public class ReportOutput extends AbstractActionOutput implements Serializable
|
||||
{
|
||||
private Integer totalRecordCount;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for totalRecordCount
|
||||
*******************************************************************************/
|
||||
public Integer getTotalRecordCount()
|
||||
{
|
||||
return (this.totalRecordCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for totalRecordCount
|
||||
*******************************************************************************/
|
||||
public void setTotalRecordCount(Integer totalRecordCount)
|
||||
{
|
||||
this.totalRecordCount = totalRecordCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for totalRecordCount
|
||||
*******************************************************************************/
|
||||
public ReportOutput withTotalRecordCount(Integer totalRecordCount)
|
||||
{
|
||||
this.totalRecordCount = totalRecordCount;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -1,217 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Full definition of a pivot table - its rows, columns, and values.
|
||||
*******************************************************************************/
|
||||
public class PivotTableDefinition implements Cloneable, Serializable
|
||||
{
|
||||
private List<PivotTableGroupBy> rows;
|
||||
private List<PivotTableGroupBy> columns;
|
||||
private List<PivotTableValue> values;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
protected PivotTableDefinition clone() throws CloneNotSupportedException
|
||||
{
|
||||
PivotTableDefinition clone = (PivotTableDefinition) super.clone();
|
||||
|
||||
if(rows != null)
|
||||
{
|
||||
clone.rows = new ArrayList<>();
|
||||
for(PivotTableGroupBy row : rows)
|
||||
{
|
||||
clone.rows.add(row.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if(columns != null)
|
||||
{
|
||||
clone.columns = new ArrayList<>();
|
||||
for(PivotTableGroupBy column : columns)
|
||||
{
|
||||
clone.columns.add(column.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if(values != null)
|
||||
{
|
||||
clone.values = new ArrayList<>();
|
||||
for(PivotTableValue value : values)
|
||||
{
|
||||
clone.values.add(value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
return (clone);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public PivotTableDefinition withRow(PivotTableGroupBy row)
|
||||
{
|
||||
if(this.rows == null)
|
||||
{
|
||||
this.rows = new ArrayList<>();
|
||||
}
|
||||
this.rows.add(row);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public PivotTableDefinition withColumn(PivotTableGroupBy column)
|
||||
{
|
||||
if(this.columns == null)
|
||||
{
|
||||
this.columns = new ArrayList<>();
|
||||
}
|
||||
this.columns.add(column);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public PivotTableDefinition withValue(PivotTableValue value)
|
||||
{
|
||||
if(this.values == null)
|
||||
{
|
||||
this.values = new ArrayList<>();
|
||||
}
|
||||
this.values.add(value);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for rows
|
||||
*******************************************************************************/
|
||||
public List<PivotTableGroupBy> getRows()
|
||||
{
|
||||
return (this.rows);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for rows
|
||||
*******************************************************************************/
|
||||
public void setRows(List<PivotTableGroupBy> rows)
|
||||
{
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for rows
|
||||
*******************************************************************************/
|
||||
public PivotTableDefinition withRows(List<PivotTableGroupBy> rows)
|
||||
{
|
||||
this.rows = rows;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for columns
|
||||
*******************************************************************************/
|
||||
public List<PivotTableGroupBy> getColumns()
|
||||
{
|
||||
return (this.columns);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for columns
|
||||
*******************************************************************************/
|
||||
public void setColumns(List<PivotTableGroupBy> columns)
|
||||
{
|
||||
this.columns = columns;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for columns
|
||||
*******************************************************************************/
|
||||
public PivotTableDefinition withColumns(List<PivotTableGroupBy> columns)
|
||||
{
|
||||
this.columns = columns;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for values
|
||||
*******************************************************************************/
|
||||
public List<PivotTableValue> getValues()
|
||||
{
|
||||
return (this.values);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for values
|
||||
*******************************************************************************/
|
||||
public void setValues(List<PivotTableValue> values)
|
||||
{
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for values
|
||||
*******************************************************************************/
|
||||
public PivotTableDefinition withValues(List<PivotTableValue> values)
|
||||
{
|
||||
this.values = values;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Functions that can be applied to Values in a pivot table.
|
||||
*******************************************************************************/
|
||||
public enum PivotTableFunction
|
||||
{
|
||||
AVERAGE("Average"),
|
||||
COUNT("Count Values (COUNTA)"),
|
||||
COUNT_NUMS("Count Numbers (COUNT)"),
|
||||
MAX("Max"),
|
||||
MIN("Min"),
|
||||
PRODUCT("Product"),
|
||||
STD_DEV("StdDev"),
|
||||
STD_DEVP("StdDevp"),
|
||||
SUM("Sum"),
|
||||
VAR("Var"),
|
||||
VARP("Varp");
|
||||
|
||||
|
||||
private final String label;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
PivotTableFunction(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return label;
|
||||
}
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Either a row or column grouping in a pivot table. e.g., a field plus
|
||||
** sorting details, plus showTotals boolean.
|
||||
*******************************************************************************/
|
||||
public class PivotTableGroupBy implements Cloneable, Serializable
|
||||
{
|
||||
private String fieldName;
|
||||
private PivotTableOrderBy orderBy;
|
||||
private boolean showTotals;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldName
|
||||
*******************************************************************************/
|
||||
public String getFieldName()
|
||||
{
|
||||
return (this.fieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fieldName
|
||||
*******************************************************************************/
|
||||
public void setFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fieldName
|
||||
*******************************************************************************/
|
||||
public PivotTableGroupBy withFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for orderBy
|
||||
*******************************************************************************/
|
||||
public PivotTableOrderBy getOrderBy()
|
||||
{
|
||||
return (this.orderBy);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for orderBy
|
||||
*******************************************************************************/
|
||||
public void setOrderBy(PivotTableOrderBy orderBy)
|
||||
{
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for orderBy
|
||||
*******************************************************************************/
|
||||
public PivotTableGroupBy withOrderBy(PivotTableOrderBy orderBy)
|
||||
{
|
||||
this.orderBy = orderBy;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for showTotals
|
||||
*******************************************************************************/
|
||||
public boolean getShowTotals()
|
||||
{
|
||||
return (this.showTotals);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for showTotals
|
||||
*******************************************************************************/
|
||||
public void setShowTotals(boolean showTotals)
|
||||
{
|
||||
this.showTotals = showTotals;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for showTotals
|
||||
*******************************************************************************/
|
||||
public PivotTableGroupBy withShowTotals(boolean showTotals)
|
||||
{
|
||||
this.showTotals = showTotals;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public PivotTableGroupBy clone() throws CloneNotSupportedException
|
||||
{
|
||||
PivotTableGroupBy clone = (PivotTableGroupBy) super.clone();
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** How a group-by (rows or columns) should be sorted.
|
||||
*******************************************************************************/
|
||||
public class PivotTableOrderBy implements Serializable
|
||||
{
|
||||
// todo - implement, but only if POI supports (or we build our own support...)
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.reporting.pivottable;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** a value (e.g., field name + function) used in a pivot table
|
||||
*******************************************************************************/
|
||||
public class PivotTableValue implements Cloneable, Serializable
|
||||
{
|
||||
private String fieldName;
|
||||
private PivotTableFunction function;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldName
|
||||
*******************************************************************************/
|
||||
public String getFieldName()
|
||||
{
|
||||
return (this.fieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fieldName
|
||||
*******************************************************************************/
|
||||
public void setFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fieldName
|
||||
*******************************************************************************/
|
||||
public PivotTableValue withFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for function
|
||||
*******************************************************************************/
|
||||
public PivotTableFunction getFunction()
|
||||
{
|
||||
return (this.function);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for function
|
||||
*******************************************************************************/
|
||||
public void setFunction(PivotTableFunction function)
|
||||
{
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for function
|
||||
*******************************************************************************/
|
||||
public PivotTableValue withFunction(PivotTableFunction function)
|
||||
{
|
||||
this.function = function;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public PivotTableValue clone() throws CloneNotSupportedException
|
||||
{
|
||||
PivotTableValue clone = (PivotTableValue) super.clone();
|
||||
return clone;
|
||||
}
|
||||
|
||||
}
|
@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
@ -140,24 +139,6 @@ public class DeleteInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluently add 1 primary key to the delete input
|
||||
**
|
||||
*******************************************************************************/
|
||||
public DeleteInput withPrimaryKey(Serializable primaryKey)
|
||||
{
|
||||
if(primaryKeys == null)
|
||||
{
|
||||
primaryKeys = new ArrayList<>();
|
||||
}
|
||||
|
||||
primaryKeys.add(primaryKey);
|
||||
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for ids
|
||||
**
|
||||
|
@ -112,7 +112,7 @@ public class InsertInput extends AbstractTableActionInput
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public InsertInput withRecordEntities(List<? extends QRecordEntity> recordEntityList)
|
||||
public InsertInput withRecordEntities(List<QRecordEntity> recordEntityList)
|
||||
{
|
||||
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
|
||||
{
|
||||
|
@ -22,7 +22,6 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@ -38,18 +37,12 @@ import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
@ -67,23 +60,11 @@ public class JoinsContext
|
||||
private final String mainTableName;
|
||||
private final List<QueryJoin> queryJoins;
|
||||
|
||||
private final QQueryFilter securityFilter;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// pointer either at securityFilter, or at a sub-filter within it, for when we're doing a recursive build-out of multi-locks //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private QQueryFilter securityFilterCursor;
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
// note - will have entries for all tables, not just aliases. //
|
||||
////////////////////////////////////////////////////////////////
|
||||
private final Map<String, String> aliasToTableNameMap = new HashMap<>();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// we will get a TON of more output if this gets turned up, so be cautious //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
private Level logLevel = Level.OFF;
|
||||
private Level logLevelForFilter = Level.OFF;
|
||||
private Level logLevel = Level.OFF;
|
||||
|
||||
|
||||
|
||||
@ -93,225 +74,54 @@ public class JoinsContext
|
||||
*******************************************************************************/
|
||||
public JoinsContext(QInstance instance, String tableName, List<QueryJoin> queryJoins, QQueryFilter filter) throws QException
|
||||
{
|
||||
log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
|
||||
this.instance = instance;
|
||||
this.mainTableName = tableName;
|
||||
this.queryJoins = new MutableList<>(queryJoins);
|
||||
this.securityFilter = new QQueryFilter();
|
||||
this.securityFilterCursor = this.securityFilter;
|
||||
|
||||
// log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
|
||||
dumpDebug(true, false);
|
||||
|
||||
for(QueryJoin queryJoin : this.queryJoins)
|
||||
{
|
||||
log("Processing input query join", logPair("joinTable", queryJoin.getJoinTable()), logPair("alias", queryJoin.getAlias()), logPair("baseTableOrAlias", queryJoin.getBaseTableOrAlias()), logPair("joinMetaDataName", () -> queryJoin.getJoinMetaData().getName()));
|
||||
processQueryJoin(queryJoin);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure that all tables specified in filter columns are being brought into the query as joins //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ensureFilterIsRepresented(filter);
|
||||
logFilter("After ensureFilterIsRepresented:", securityFilter);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// ensure that any record locks on the main table, which require a join, are present //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
MultiRecordSecurityLock multiRecordSecurityLock = RecordSecurityLockFilters.filterForReadLockTree(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()));
|
||||
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
|
||||
{
|
||||
ensureRecordSecurityLockIsRepresented(tableName, tableName, lock, null);
|
||||
logFilter("After ensureRecordSecurityLockIsRepresented[fieldName=" + lock.getFieldName() + "]:", securityFilter);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure that all joins in the query have meta data specified //
|
||||
// e.g., a user-added join may just specify the join-table //
|
||||
// or a join implicitly added from a filter may also not have its join meta data //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
fillInMissingJoinMetaData();
|
||||
logFilter("After fillInMissingJoinMetaData:", securityFilter);
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// ensure any joins that contribute a recordLock are present //
|
||||
///////////////////////////////////////////////////////////////
|
||||
ensureAllJoinRecordSecurityLocksAreRepresented(instance);
|
||||
logFilter("After ensureAllJoinRecordSecurityLocksAreRepresented:", securityFilter);
|
||||
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())))
|
||||
{
|
||||
ensureRecordSecurityLockIsRepresented(instance, tableName, recordSecurityLock);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there were any security filters built, then put those into the input filter //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
addSecurityFiltersToInputFilter(filter);
|
||||
ensureFilterIsRepresented(filter);
|
||||
|
||||
addJoinsFromExposedJoinPaths();
|
||||
|
||||
/* todo!!
|
||||
for(QueryJoin queryJoin : queryJoins)
|
||||
{
|
||||
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))
|
||||
{
|
||||
// addCriteriaForRecordSecurityLock(instance, session, joinTable, securityCriteria, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
log("Constructed JoinsContext", logPair("mainTableName", this.mainTableName), logPair("queryJoins", this.queryJoins.stream().map(qj -> qj.getJoinTable()).collect(Collectors.joining(","))));
|
||||
log("", logPair("securityFilter", securityFilter));
|
||||
log("", logPair("fullFilter", filter));
|
||||
dumpDebug(false, true);
|
||||
// log("--- END ------------------------------------------------------------------------");
|
||||
log("--- END ------------------------------------------------------------------------");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Update the input filter with any security filters that were built.
|
||||
*******************************************************************************/
|
||||
private void addSecurityFiltersToInputFilter(QQueryFilter filter)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's no security filter criteria (including sub-filters), return w/ noop //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeIsEmpty(securityFilter.getSubFilters()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// if the input filter is an OR we need to replace it with a new AND //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
if(filter.getBooleanOperator().equals(QQueryFilter.BooleanOperator.OR))
|
||||
{
|
||||
List<QFilterCriteria> originalCriteria = filter.getCriteria();
|
||||
List<QQueryFilter> originalSubFilters = filter.getSubFilters();
|
||||
|
||||
QQueryFilter replacementFilter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
replacementFilter.setCriteria(originalCriteria);
|
||||
replacementFilter.setSubFilters(originalSubFilters);
|
||||
|
||||
filter.setCriteria(new ArrayList<>());
|
||||
filter.setSubFilters(new ArrayList<>());
|
||||
filter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
|
||||
filter.addSubFilter(replacementFilter);
|
||||
}
|
||||
|
||||
filter.addSubFilter(securityFilter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** In case we've added any joins to the query that have security locks which
|
||||
** weren't previously added to the query, add them now. basically, this is
|
||||
** calling ensureRecordSecurityLockIsRepresented for each queryJoin.
|
||||
*******************************************************************************/
|
||||
private void ensureAllJoinRecordSecurityLocksAreRepresented(QInstance instance) throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// avoid concurrent modification exceptions by doing a double-loop and breaking the inner any time anything gets added //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Set<QueryJoin> processedQueryJoins = new HashSet<>();
|
||||
boolean addedAnyThisIteration = true;
|
||||
while(addedAnyThisIteration)
|
||||
{
|
||||
addedAnyThisIteration = false;
|
||||
|
||||
for(QueryJoin queryJoin : this.queryJoins)
|
||||
{
|
||||
boolean addedAnyForThisJoin = false;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// avoid double-processing the same query join //
|
||||
// or adding security filters for a join who was only added to the query so that we could add locks (an ImplicitQueryJoinForSecurityLock) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(processedQueryJoins.contains(queryJoin) || queryJoin instanceof ImplicitQueryJoinForSecurityLock)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
processedQueryJoins.add(queryJoin);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// process all locks on this join's join-table. keep track if any new joins were added //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
|
||||
|
||||
MultiRecordSecurityLock multiRecordSecurityLock = RecordSecurityLockFilters.filterForReadLockTree(CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()));
|
||||
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
|
||||
{
|
||||
List<QueryJoin> addedQueryJoins = ensureRecordSecurityLockIsRepresented(joinTable.getName(), queryJoin.getJoinTableOrItsAlias(), lock, queryJoin);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if any joins were added by this call, add them to the set of processed ones, so they don't get re-processed. //
|
||||
// also mark the flag that any were added for this join, to manage the double-looping //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(addedQueryJoins))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make all new joins added in that method be of the same type (inner/left/etc) as the query join they are connected to //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QueryJoin addedQueryJoin : addedQueryJoins)
|
||||
{
|
||||
addedQueryJoin.setType(queryJoin.getType());
|
||||
}
|
||||
|
||||
processedQueryJoins.addAll(addedQueryJoins);
|
||||
addedAnyForThisJoin = true;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if any new joins were added, we need to break the inner-loop, and continue the outer loop //
|
||||
// e.g., to process the next query join (but we can't just go back to the foreach queryJoin, //
|
||||
// because it would fail with concurrent modification) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(addedAnyForThisJoin)
|
||||
{
|
||||
addedAnyThisIteration = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a given recordSecurityLock on a given table (with a possible alias),
|
||||
** make sure that if any joins are needed to get to the lock, that they are in the query.
|
||||
**
|
||||
** returns the list of query joins that were added, if any were added
|
||||
*******************************************************************************/
|
||||
private List<QueryJoin> ensureRecordSecurityLockIsRepresented(String tableName, String tableNameOrAlias, RecordSecurityLock recordSecurityLock, QueryJoin sourceQueryJoin) throws QException
|
||||
private void ensureRecordSecurityLockIsRepresented(QInstance instance, String tableName, RecordSecurityLock recordSecurityLock) throws QException
|
||||
{
|
||||
List<QueryJoin> addedQueryJoins = new ArrayList<>();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// if this lock is a multi-lock, then recursively process its child-locks //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
|
||||
{
|
||||
log("Processing MultiRecordSecurityLock...");
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make a new level in the filter-tree - storing old cursor, and updating cursor to point at new level //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QQueryFilter oldSecurityFilterCursor = this.securityFilterCursor;
|
||||
QQueryFilter nextLevelSecurityFilter = new QQueryFilter();
|
||||
this.securityFilterCursor.addSubFilter(nextLevelSecurityFilter);
|
||||
this.securityFilterCursor = nextLevelSecurityFilter;
|
||||
|
||||
///////////////////////////////////////
|
||||
// set the boolean operator to match //
|
||||
///////////////////////////////////////
|
||||
nextLevelSecurityFilter.setBooleanOperator(multiRecordSecurityLock.getOperator().toFilterOperator());
|
||||
|
||||
//////////////////////
|
||||
// process children //
|
||||
//////////////////////
|
||||
for(RecordSecurityLock childLock : CollectionUtils.nonNullList(multiRecordSecurityLock.getLocks()))
|
||||
{
|
||||
log(" - Recursive call for childLock: " + childLock);
|
||||
addedQueryJoins.addAll(ensureRecordSecurityLockIsRepresented(tableName, tableNameOrAlias, childLock, sourceQueryJoin));
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// restore cursor //
|
||||
////////////////////
|
||||
this.securityFilterCursor = oldSecurityFilterCursor;
|
||||
|
||||
return addedQueryJoins;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// A join name chain is going to look like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where its security field is): //
|
||||
// ok - so - the join name chain is going to be like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
|
||||
// - securityFieldName = order.clientId //
|
||||
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
|
||||
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
|
||||
@ -319,30 +129,30 @@ public class JoinsContext
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
|
||||
Collections.reverse(joinNameChain);
|
||||
log("Evaluating recordSecurityLock. Join name chain is of length: " + joinNameChain.size(), logPair("tableNameOrAlias", tableNameOrAlias), logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
|
||||
log("Evaluating recordSecurityLock", logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
|
||||
|
||||
QTableMetaData tmpTable = instance.getTable(tableName);
|
||||
String securityFieldTableAlias = tableNameOrAlias;
|
||||
String baseTableOrAlias = tableNameOrAlias;
|
||||
|
||||
boolean chainIsInner = true;
|
||||
if(sourceQueryJoin != null && QueryJoin.Type.isOuter(sourceQueryJoin.getType()))
|
||||
{
|
||||
chainIsInner = false;
|
||||
}
|
||||
QTableMetaData tmpTable = instance.getTable(mainTableName);
|
||||
|
||||
for(String joinName : joinNameChain)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check the joins currently in the query - if any are THIS join, then we don't need to add one //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QueryJoin> matchingQueryJoins = this.queryJoins.stream().filter(queryJoin ->
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check the joins currently in the query - if any are for this table, then we don't need to add one //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QueryJoin> matchingJoins = this.queryJoins.stream().filter(queryJoin ->
|
||||
{
|
||||
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
|
||||
QJoinMetaData joinMetaData = null;
|
||||
if(queryJoin.getJoinMetaData() != null)
|
||||
{
|
||||
joinMetaData = queryJoin.getJoinMetaData();
|
||||
}
|
||||
else
|
||||
{
|
||||
joinMetaData = findJoinMetaData(instance, tableName, queryJoin.getJoinTable());
|
||||
}
|
||||
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
|
||||
}).toList();
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(matchingQueryJoins))
|
||||
if(CollectionUtils.nullSafeHasContents(matchingJoins))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - if a user added a join as an outer type, we need to change it to be inner, for the security purpose. //
|
||||
@ -350,40 +160,11 @@ public class JoinsContext
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
log("- skipping join already in the query", logPair("joinName", joinName));
|
||||
|
||||
QueryJoin matchedQueryJoin = matchingQueryJoins.get(0);
|
||||
|
||||
if(matchedQueryJoin.getType().equals(QueryJoin.Type.LEFT) || matchedQueryJoin.getType().equals(QueryJoin.Type.RIGHT))
|
||||
{
|
||||
chainIsInner = false;
|
||||
}
|
||||
|
||||
/* ?? todo ??
|
||||
if(matchedQueryJoin.getType().equals(QueryJoin.Type.LEFT) || matchedQueryJoin.getType().equals(QueryJoin.Type.RIGHT))
|
||||
if(matchingJoins.get(0).getType().equals(QueryJoin.Type.LEFT) || matchingJoins.get(0).getType().equals(QueryJoin.Type.RIGHT))
|
||||
{
|
||||
log("- - although... it was here as an outer - so switching it to INNER", logPair("joinName", joinName));
|
||||
matchedQueryJoin.setType(QueryJoin.Type.INNER);
|
||||
matchingJoins.get(0).setType(QueryJoin.Type.INNER);
|
||||
}
|
||||
*/
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// as we're walking from tmpTable to the table which ultimately has the security key field, //
|
||||
// if the queryJoin we just found is joining out to tmpTable, then we need to advance tmpTable back //
|
||||
// to the queryJoin's base table - else, tmpTable advances to the matched queryJoin's joinTable //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(tmpTable.getName().equals(matchedQueryJoin.getJoinTable()))
|
||||
{
|
||||
securityFieldTableAlias = Objects.requireNonNullElse(matchedQueryJoin.getBaseTableOrAlias(), mainTableName);
|
||||
}
|
||||
else
|
||||
{
|
||||
securityFieldTableAlias = matchedQueryJoin.getJoinTableOrItsAlias();
|
||||
}
|
||||
tmpTable = instance.getTable(securityFieldTableAlias);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set the baseTableOrAlias for the next iteration to be this join's joinTableOrAlias //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
baseTableOrAlias = securityFieldTableAlias;
|
||||
|
||||
continue;
|
||||
}
|
||||
@ -391,233 +172,20 @@ public class JoinsContext
|
||||
QJoinMetaData join = instance.getJoin(joinName);
|
||||
if(join.getLeftTable().equals(tmpTable.getName()))
|
||||
{
|
||||
securityFieldTableAlias = join.getRightTable() + "_forSecurityJoin_" + join.getName();
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock()
|
||||
.withJoinMetaData(join)
|
||||
.withType(chainIsInner ? QueryJoin.Type.INNER : QueryJoin.Type.LEFT)
|
||||
.withBaseTableOrAlias(baseTableOrAlias)
|
||||
.withAlias(securityFieldTableAlias);
|
||||
|
||||
if(securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR)
|
||||
{
|
||||
queryJoin.withType(QueryJoin.Type.LEFT);
|
||||
chainIsInner = false;
|
||||
}
|
||||
|
||||
addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)", "- ");
|
||||
addedQueryJoins.add(queryJoin);
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
this.addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)");
|
||||
tmpTable = instance.getTable(join.getRightTable());
|
||||
}
|
||||
else if(join.getRightTable().equals(tmpTable.getName()))
|
||||
{
|
||||
securityFieldTableAlias = join.getLeftTable() + "_forSecurityJoin_" + join.getName();
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock()
|
||||
.withJoinMetaData(join.flip())
|
||||
.withType(chainIsInner ? QueryJoin.Type.INNER : QueryJoin.Type.LEFT)
|
||||
.withBaseTableOrAlias(baseTableOrAlias)
|
||||
.withAlias(securityFieldTableAlias);
|
||||
|
||||
if(securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR)
|
||||
{
|
||||
queryJoin.withType(QueryJoin.Type.LEFT);
|
||||
chainIsInner = false;
|
||||
}
|
||||
|
||||
addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)", "- ");
|
||||
addedQueryJoins.add(queryJoin);
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
|
||||
this.addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)");
|
||||
tmpTable = instance.getTable(join.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
dumpDebug(false, true);
|
||||
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for the next iteration of the loop, set the next join's baseTableOrAlias to be the alias we just created //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
baseTableOrAlias = securityFieldTableAlias;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// now that we know the joins/tables are in the query, add to the security filter //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryJoin lastAddedQueryJoin = addedQueryJoins.isEmpty() ? null : addedQueryJoins.get(addedQueryJoins.size() - 1);
|
||||
if(sourceQueryJoin != null && lastAddedQueryJoin == null)
|
||||
{
|
||||
lastAddedQueryJoin = sourceQueryJoin;
|
||||
}
|
||||
addSubFilterForRecordSecurityLock(recordSecurityLock, tmpTable, securityFieldTableAlias, !chainIsInner, lastAddedQueryJoin);
|
||||
|
||||
log("Finished evaluating recordSecurityLock");
|
||||
|
||||
return (addedQueryJoins);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addSubFilterForRecordSecurityLock(RecordSecurityLock recordSecurityLock, QTableMetaData table, String tableNameOrAlias, boolean isOuter, QueryJoin sourceQueryJoin)
|
||||
{
|
||||
QSession session = QContext.getQSession();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check if the key type has an all-access key, and if so, if it's set to true for the current user/session //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QSecurityKeyType securityKeyType = instance.getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
|
||||
boolean haveAllAccessKey = false;
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we have all-access on this key, then we don't need a criterion for it (as long as we're in an AND filter) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
|
||||
{
|
||||
haveAllAccessKey = true;
|
||||
|
||||
if(sourceQueryJoin != null)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// in case the queryJoin object is re-used between queries, and its security criteria need to be different (!!), reset it //
|
||||
// this can be exposed in tests - maybe not entirely expected in real-world, but seems safe enough //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
sourceQueryJoin.withSecurityCriteria(new ArrayList<>());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're in an AND filter, then we don't need a criteria for this lock, so return. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean inAnAndFilter = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.AND;
|
||||
if(inAnAndFilter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for locks w/o a join chain, the lock fieldName will simply be a field on the table. //
|
||||
// so just prepend that with the tableNameOrAlias. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
|
||||
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// else, expect a "table.field" in the lock fieldName - but we want to replace //
|
||||
// the table name part with a possible alias that we took in. //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
String[] parts = recordSecurityLock.getFieldName().split("\\.");
|
||||
if(parts.length != 2)
|
||||
{
|
||||
dumpDebug(false, true);
|
||||
throw new IllegalArgumentException("Mal-formatted recordSecurityLock fieldName for lock with joinNameChain in query: " + fieldName);
|
||||
}
|
||||
fieldName = tableNameOrAlias + "." + parts[1];
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else - get the key values from the session and decide what kind of criterion to build //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
QQueryFilter lockFilter = new QQueryFilter();
|
||||
List<QFilterCriteria> lockCriteria = new ArrayList<>();
|
||||
lockFilter.setCriteria(lockCriteria);
|
||||
|
||||
QFieldType type = QFieldType.INTEGER;
|
||||
try
|
||||
{
|
||||
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = getFieldAndTableNameOrAlias(fieldName);
|
||||
type = fieldAndTableNameOrAlias.field().getType();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Error getting field type... Trying Integer", e);
|
||||
}
|
||||
|
||||
if(haveAllAccessKey)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we have an all access key (but we got here because we're part of an OR query), then //
|
||||
// write a criterion that will always be true - e.g., field=field //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.TRUE));
|
||||
}
|
||||
else
|
||||
{
|
||||
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), type);
|
||||
if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else, if no user/session values, and null-value behavior is deny, then setup a FALSE condition, to allow no rows. //
|
||||
// todo - maybe avoid running the whole query - as you're not allowed ANY records (based on boolean tree down to this point) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.FALSE));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else, if user/session has some values, build an IN rule - //
|
||||
// noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
|
||||
}
|
||||
else
|
||||
{
|
||||
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, securityKeyValues));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's a sourceQueryJoin, then set the lockCriteria on that join - so it gets written into the JOIN ... ON clause //
|
||||
// ... unless we're writing an OR filter. then we need the condition in the WHERE clause //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean doNotPutCriteriaInJoinOn = securityFilterCursor.getBooleanOperator() == QQueryFilter.BooleanOperator.OR;
|
||||
if(sourceQueryJoin != null && !doNotPutCriteriaInJoinOn)
|
||||
{
|
||||
sourceQueryJoin.withSecurityCriteria(lockCriteria);
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we used to add an OR IS NULL for cases of an outer-join - but instead, this is now handled by putting the lockCriteria //
|
||||
// into the join (see above) - so this check is probably deprecated. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/*
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this field is on the outer side of an outer join, then if we do a straight filter on it, then we're basically //
|
||||
// nullifying the outer join... so for an outer join use-case, OR the security field criteria with a primary-key IS NULL //
|
||||
// which will make missing rows from the join be found. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(isOuter)
|
||||
{
|
||||
if(table == null)
|
||||
{
|
||||
table = QContext.getQInstance().getTable(aliasToTableNameMap.get(tableNameOrAlias));
|
||||
}
|
||||
|
||||
lockFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
lockFilter.addCriteria(new QFilterCriteria(tableNameOrAlias + "." + table.getPrimaryKeyField(), QCriteriaOperator.IS_BLANK));
|
||||
}
|
||||
*/
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// If this filter isn't for a queryJoin, then just add it to the main list of security sub-filters //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
this.securityFilterCursor.addSubFilter(lockFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -629,9 +197,9 @@ public class JoinsContext
|
||||
** use this method to add to the list, instead of ever adding directly, as it's
|
||||
** important do to that process step (and we've had bugs when it wasn't done).
|
||||
*******************************************************************************/
|
||||
private void addQueryJoin(QueryJoin queryJoin, String reason, String logPrefix) throws QException
|
||||
private void addQueryJoin(QueryJoin queryJoin, String reason) throws QException
|
||||
{
|
||||
log(Objects.requireNonNullElse(logPrefix, "") + "Adding query join to context",
|
||||
log("Adding query join to context",
|
||||
logPair("reason", reason),
|
||||
logPair("joinTable", queryJoin.getJoinTable()),
|
||||
logPair("joinMetaData.name", () -> queryJoin.getJoinMetaData().getName()),
|
||||
@ -640,46 +208,34 @@ public class JoinsContext
|
||||
);
|
||||
this.queryJoins.add(queryJoin);
|
||||
processQueryJoin(queryJoin);
|
||||
dumpDebug(false, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** If there are any joins in the context that don't have a join meta data, see
|
||||
** if we can find the JoinMetaData to use for them by looking at all joins in the
|
||||
** instance, or at the main table's exposed joins, and using their join paths.
|
||||
** if we can find the JoinMetaData to use for them by looking at the main table's
|
||||
** exposed joins, and using their join paths.
|
||||
*******************************************************************************/
|
||||
private void fillInMissingJoinMetaData() throws QException
|
||||
private void addJoinsFromExposedJoinPaths() throws QException
|
||||
{
|
||||
log("Begin adding missing join meta data");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// do a double-loop, to avoid concurrent modification on the queryJoins list. //
|
||||
// that is to say, we'll loop over that list, but possibly add things to it, //
|
||||
// in which case we'll set this flag, and break the inner loop, to go again. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Set<QueryJoin> processedQueryJoins = new HashSet<>();
|
||||
boolean addedJoin;
|
||||
boolean addedJoin;
|
||||
do
|
||||
{
|
||||
addedJoin = false;
|
||||
for(QueryJoin queryJoin : queryJoins)
|
||||
{
|
||||
if(processedQueryJoins.contains(queryJoin))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
processedQueryJoins.add(queryJoin);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the join has joinMetaData, then we don't need to process it... unless it needs flipped //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
|
||||
if(joinMetaData != null)
|
||||
{
|
||||
log("- QueryJoin already has joinMetaData", logPair("joinMetaDataName", joinMetaData.getName()));
|
||||
|
||||
boolean isJoinLeftTableInQuery = false;
|
||||
String joinMetaDataLeftTable = joinMetaData.getLeftTable();
|
||||
if(joinMetaDataLeftTable.equals(mainTableName))
|
||||
@ -709,7 +265,7 @@ public class JoinsContext
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
if(!isJoinLeftTableInQuery)
|
||||
{
|
||||
log("- - Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
|
||||
log("Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
|
||||
queryJoin.setJoinMetaData(joinMetaData.flip());
|
||||
}
|
||||
}
|
||||
@ -719,13 +275,11 @@ public class JoinsContext
|
||||
// try to find a direct join between the main table and this table. //
|
||||
// if one is found, then put it (the meta data) on the query join. //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
log("- QueryJoin doesn't have metaData - looking for it", logPair("joinTableOrItsAlias", queryJoin.getJoinTableOrItsAlias()));
|
||||
|
||||
String baseTableName = Objects.requireNonNullElse(resolveTableNameOrAliasToTableName(queryJoin.getBaseTableOrAlias()), mainTableName);
|
||||
QJoinMetaData found = findJoinMetaData(baseTableName, queryJoin.getJoinTable(), true);
|
||||
QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
|
||||
if(found != null)
|
||||
{
|
||||
log("- - Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
|
||||
log("Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
|
||||
queryJoin.setJoinMetaData(found);
|
||||
}
|
||||
else
|
||||
@ -739,7 +293,7 @@ public class JoinsContext
|
||||
{
|
||||
if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable()))
|
||||
{
|
||||
log("- - Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
|
||||
log("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// loop backward through the join path (from the joinTable back to the main table) //
|
||||
@ -750,7 +304,6 @@ public class JoinsContext
|
||||
{
|
||||
String joinName = exposedJoin.getJoinPath().get(i);
|
||||
QJoinMetaData joinToAdd = instance.getJoin(joinName);
|
||||
log("- - - evaluating joinPath element", logPair("i", i), logPair("joinName", joinName));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// get the name from the opposite side of the join (flipping it if needed) //
|
||||
@ -779,22 +332,15 @@ public class JoinsContext
|
||||
queryJoin.setBaseTableOrAlias(nextTable);
|
||||
}
|
||||
queryJoin.setJoinMetaData(joinToAdd);
|
||||
log("- - - - this is the last element in the join path, so setting this joinMetaData on the original queryJoin");
|
||||
}
|
||||
else
|
||||
{
|
||||
QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd);
|
||||
queryJoinToAdd.setType(queryJoin.getType());
|
||||
addedAnyQueryJoins = true;
|
||||
log("- - - - this is not the last element in the join path, so adding a new query join:");
|
||||
addQueryJoin(queryJoinToAdd, "forExposedJoin", "- - - - - - ");
|
||||
dumpDebug(false, false);
|
||||
this.addQueryJoin(queryJoinToAdd, "forExposedJoin");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log("- - - - join doesn't need added to the query");
|
||||
}
|
||||
|
||||
tmpTable = nextTable;
|
||||
}
|
||||
@ -815,7 +361,6 @@ public class JoinsContext
|
||||
}
|
||||
while(addedJoin);
|
||||
|
||||
log("Done adding missing join meta data");
|
||||
}
|
||||
|
||||
|
||||
@ -825,12 +370,12 @@ public class JoinsContext
|
||||
*******************************************************************************/
|
||||
private boolean doesJoinNeedAddedToQuery(String joinName)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// look at all queryJoins already in context - if any have this join's name, and aren't implicit-security joins, then we don't need this join... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// look at all queryJoins already in context - if any have this join's name, then we don't need this join... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QueryJoin queryJoin : queryJoins)
|
||||
{
|
||||
if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName) && !(queryJoin instanceof ImplicitQueryJoinForSecurityLock))
|
||||
if(queryJoin.getJoinMetaData() != null && queryJoin.getJoinMetaData().getName().equals(joinName))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
@ -850,7 +395,6 @@ public class JoinsContext
|
||||
String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias();
|
||||
if(aliasToTableNameMap.containsKey(tableNameOrAlias))
|
||||
{
|
||||
dumpDebug(false, true);
|
||||
throw (new QException("Duplicate table name or alias: " + tableNameOrAlias));
|
||||
}
|
||||
aliasToTableNameMap.put(tableNameOrAlias, joinTable.getName());
|
||||
@ -895,7 +439,6 @@ public class JoinsContext
|
||||
String[] parts = fieldName.split("\\.");
|
||||
if(parts.length != 2)
|
||||
{
|
||||
dumpDebug(false, true);
|
||||
throw new IllegalArgumentException("Mal-formatted field name in query: " + fieldName);
|
||||
}
|
||||
|
||||
@ -906,7 +449,6 @@ public class JoinsContext
|
||||
QTableMetaData table = instance.getTable(tableName);
|
||||
if(table == null)
|
||||
{
|
||||
dumpDebug(false, true);
|
||||
throw new IllegalArgumentException("Could not find table [" + tableName + "] in instance for query");
|
||||
}
|
||||
return new FieldAndTableNameOrAlias(table.getField(baseFieldName), tableOrAlias);
|
||||
@ -961,17 +503,17 @@ public class JoinsContext
|
||||
|
||||
for(String filterTable : filterTables)
|
||||
{
|
||||
log("Evaluating filter", logPair("filterTable", filterTable));
|
||||
log("Evaluating filterTable", logPair("filterTable", filterTable));
|
||||
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
|
||||
{
|
||||
log("- table not in query - adding a join for it", logPair("filterTable", filterTable));
|
||||
log("- table not in query - adding it", logPair("filterTable", filterTable));
|
||||
boolean found = false;
|
||||
for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values())
|
||||
{
|
||||
QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join);
|
||||
if(queryJoin != null)
|
||||
{
|
||||
addQueryJoin(queryJoin, "forFilter (join found in instance)", "- - ");
|
||||
this.addQueryJoin(queryJoin, "forFilter (join found in instance)");
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@ -980,13 +522,9 @@ public class JoinsContext
|
||||
if(!found)
|
||||
{
|
||||
QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
|
||||
addQueryJoin(queryJoin, "forFilter (join not found in instance)", "- - ");
|
||||
this.addQueryJoin(queryJoin, "forFilter (join not found in instance)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log("- table is already in query - not adding any joins", logPair("filterTable", filterTable));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1028,11 +566,6 @@ public class JoinsContext
|
||||
getTableNameFromFieldNameAndAddToSet(criteria.getOtherFieldName(), filterTables);
|
||||
}
|
||||
|
||||
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(filter.getOrderBys()))
|
||||
{
|
||||
getTableNameFromFieldNameAndAddToSet(orderBy.getFieldName(), filterTables);
|
||||
}
|
||||
|
||||
for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters()))
|
||||
{
|
||||
populateFilterTablesSet(subFilter, filterTables);
|
||||
@ -1059,7 +592,7 @@ public class JoinsContext
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QJoinMetaData findJoinMetaData(String baseTableName, String joinTableName, boolean useExposedJoins)
|
||||
public QJoinMetaData findJoinMetaData(QInstance instance, String baseTableName, String joinTableName)
|
||||
{
|
||||
List<QJoinMetaData> matches = new ArrayList<>();
|
||||
if(baseTableName != null)
|
||||
@ -1111,29 +644,7 @@ public class JoinsContext
|
||||
}
|
||||
else if(matches.size() > 1)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// if we found more than one join, but we're allowed to useExposedJoins, then //
|
||||
// see if we can tell which match to used based on the table's exposed joins //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(useExposedJoins)
|
||||
{
|
||||
QTableMetaData mainTable = QContext.getQInstance().getTable(mainTableName);
|
||||
for(ExposedJoin exposedJoin : mainTable.getExposedJoins())
|
||||
{
|
||||
if(exposedJoin.getJoinTable().equals(joinTableName))
|
||||
{
|
||||
// todo ... is it wrong to always use 0??
|
||||
return instance.getJoin(exposedJoin.getJoinPath().get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// if we couldn't figure it out, then throw. //
|
||||
///////////////////////////////////////////////
|
||||
dumpDebug(false, true);
|
||||
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "] "
|
||||
+ (useExposedJoins ? "(and exposed joins didn't clarify which one to use). " : "") + "Specify which one in your QueryJoin."));
|
||||
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin."));
|
||||
}
|
||||
|
||||
return (null);
|
||||
@ -1158,79 +669,4 @@ public class JoinsContext
|
||||
LOG.log(logLevel, message, null, logPairs);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void logFilter(String message, QQueryFilter filter)
|
||||
{
|
||||
if(logLevelForFilter.equals(Level.OFF))
|
||||
{
|
||||
return;
|
||||
}
|
||||
System.out.println(message + "\n" + filter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Print (to stdout, for easier reading) the object in a big table format for
|
||||
** debugging. Happens any time logLevel is > OFF. Not meant for loggly.
|
||||
*******************************************************************************/
|
||||
private void dumpDebug(boolean isStart, boolean isEnd)
|
||||
{
|
||||
if(logLevel.equals(Level.OFF))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int sm = 8;
|
||||
int md = 30;
|
||||
int lg = 50;
|
||||
int overhead = 14;
|
||||
int full = sm + 3 * md + lg + overhead;
|
||||
|
||||
if(isStart)
|
||||
{
|
||||
System.out.println("\n" + StringUtils.safeTruncate("--- Start [main table: " + this.mainTableName + "] " + "-".repeat(full), full));
|
||||
}
|
||||
|
||||
StringBuilder rs = new StringBuilder();
|
||||
String formatString = "| %-" + md + "s | %-" + md + "s %-" + md + "s | %-" + lg + "s | %-" + sm + "s |\n";
|
||||
rs.append(String.format(formatString, "Base Table", "Join Table", "(Alias)", "Join Meta Data", "Type"));
|
||||
String dashesLg = "-".repeat(lg);
|
||||
String dashesMd = "-".repeat(md);
|
||||
String dashesSm = "-".repeat(sm);
|
||||
rs.append(String.format(formatString, dashesMd, dashesMd, dashesMd, dashesLg, dashesSm));
|
||||
if(CollectionUtils.nullSafeHasContents(queryJoins))
|
||||
{
|
||||
for(QueryJoin queryJoin : queryJoins)
|
||||
{
|
||||
rs.append(String.format(
|
||||
formatString,
|
||||
StringUtils.hasContent(queryJoin.getBaseTableOrAlias()) ? StringUtils.safeTruncate(queryJoin.getBaseTableOrAlias(), md) : "--",
|
||||
StringUtils.safeTruncate(queryJoin.getJoinTable(), md),
|
||||
(StringUtils.hasContent(queryJoin.getAlias()) ? "(" + StringUtils.safeTruncate(queryJoin.getAlias(), md - 2) + ")" : ""),
|
||||
queryJoin.getJoinMetaData() == null ? "--" : StringUtils.safeTruncate(queryJoin.getJoinMetaData().getName(), lg),
|
||||
queryJoin.getType()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
rs.append(String.format(formatString, "-empty-", "", "", "", ""));
|
||||
}
|
||||
|
||||
System.out.print(rs);
|
||||
|
||||
System.out.println(securityFilter);
|
||||
|
||||
if(isEnd)
|
||||
{
|
||||
System.out.println(StringUtils.safeTruncate("--- End " + "-".repeat(full), full) + "\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
System.out.println(StringUtils.safeTruncate("-".repeat(full), full));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,5 @@ public enum QCriteriaOperator
|
||||
IS_BLANK,
|
||||
IS_NOT_BLANK,
|
||||
BETWEEN,
|
||||
NOT_BETWEEN,
|
||||
TRUE,
|
||||
FALSE
|
||||
NOT_BETWEEN
|
||||
}
|
||||
|
@ -306,11 +306,6 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
if(fieldName == null)
|
||||
{
|
||||
return ("<null-field-criteria>");
|
||||
}
|
||||
|
||||
StringBuilder rs = new StringBuilder(fieldName);
|
||||
try
|
||||
{
|
||||
|
@ -25,18 +25,12 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.FilterVariableExpression;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -143,7 +137,7 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** recursively look at both this filter, and any sub-filters it may have.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean hasAnyCriteria()
|
||||
{
|
||||
@ -156,7 +150,7 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
{
|
||||
for(QQueryFilter subFilter : subFilters)
|
||||
{
|
||||
if(subFilter != null && subFilter.hasAnyCriteria())
|
||||
if(subFilter.hasAnyCriteria())
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
@ -366,44 +360,23 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
StringBuilder rs = new StringBuilder("(");
|
||||
try
|
||||
{
|
||||
int criteriaIndex = 0;
|
||||
for(QFilterCriteria criterion : CollectionUtils.nonNullList(criteria))
|
||||
{
|
||||
if(criteriaIndex > 0)
|
||||
{
|
||||
rs.append(" ").append(getBooleanOperator()).append(" ");
|
||||
}
|
||||
rs.append(criterion);
|
||||
criteriaIndex++;
|
||||
rs.append(criterion).append(" ").append(getBooleanOperator()).append(" ");
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(subFilters))
|
||||
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
|
||||
{
|
||||
rs.append("Sub:{");
|
||||
int subIndex = 0;
|
||||
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
|
||||
{
|
||||
if(subIndex > 0)
|
||||
{
|
||||
rs.append(" ").append(getBooleanOperator()).append(" ");
|
||||
}
|
||||
rs.append(subFilter);
|
||||
subIndex++;
|
||||
}
|
||||
rs.append("}");
|
||||
rs.append(subFilter);
|
||||
}
|
||||
|
||||
rs.append(")");
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(orderBys))
|
||||
rs.append("OrderBy[");
|
||||
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
|
||||
{
|
||||
rs.append("OrderBy[");
|
||||
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
|
||||
{
|
||||
rs.append(orderBy).append(",");
|
||||
}
|
||||
rs.append("]");
|
||||
rs.append(orderBy).append(",");
|
||||
}
|
||||
rs.append("]");
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -416,88 +389,6 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Replaces any FilterVariables' variableNames with one constructed from the field
|
||||
** name, criteria, and index, camel style
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void prepForBackend()
|
||||
{
|
||||
Map<String, Integer> fieldOperatorMap = new HashMap<>();
|
||||
for(QFilterCriteria criterion : getCriteria())
|
||||
{
|
||||
if(criterion.getValues() != null)
|
||||
{
|
||||
int criteriaIndex = 1;
|
||||
int valueIndex = 0;
|
||||
for(Serializable value : criterion.getValues())
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// keep track of what the index is for this criterion, this way if there are //
|
||||
// more than one with the same id/operator values, we can differentiate //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
String backendName = getBackendName(criterion, valueIndex);
|
||||
if(!fieldOperatorMap.containsKey(backendName))
|
||||
{
|
||||
fieldOperatorMap.put(backendName, criteriaIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
criteriaIndex = fieldOperatorMap.get(backendName) + 1;
|
||||
fieldOperatorMap.put(backendName, criteriaIndex);
|
||||
}
|
||||
|
||||
if(value instanceof FilterVariableExpression fve)
|
||||
{
|
||||
if(criteriaIndex > 1)
|
||||
{
|
||||
backendName += criteriaIndex;
|
||||
}
|
||||
fve.setVariableName(backendName);
|
||||
}
|
||||
|
||||
valueIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** builds up a backend name for a field variable expression
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getBackendName(QFilterCriteria criterion, int valueIndex)
|
||||
{
|
||||
StringBuilder backendName = new StringBuilder();
|
||||
for(String fieldNameParts : criterion.getFieldName().split("\\."))
|
||||
{
|
||||
backendName.append(StringUtils.ucFirst(fieldNameParts));
|
||||
}
|
||||
|
||||
for(String operatorParts : criterion.getOperator().name().split("_"))
|
||||
{
|
||||
backendName.append(StringUtils.ucFirst(operatorParts.toLowerCase()));
|
||||
}
|
||||
|
||||
if(criterion.getOperator().equals(QCriteriaOperator.BETWEEN) || criterion.getOperator().equals(QCriteriaOperator.NOT_BETWEEN))
|
||||
{
|
||||
if(valueIndex == 0)
|
||||
{
|
||||
backendName.append("From");
|
||||
}
|
||||
else
|
||||
{
|
||||
backendName.append("To");
|
||||
}
|
||||
}
|
||||
|
||||
return (StringUtils.lcFirst(backendName.toString()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Replace any criteria values that look like ${input.XXX} with the value of XXX
|
||||
** from the supplied inputValues map.
|
||||
@ -506,10 +397,8 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
** QQueryFilter - e.g., if it's one that defined in metaData, and that we don't
|
||||
** want to be (permanently) changed!!
|
||||
*******************************************************************************/
|
||||
public void interpretValues(Map<String, Serializable> inputValues) throws QException
|
||||
public void interpretValues(Map<String, Serializable> inputValues)
|
||||
{
|
||||
List<Exception> caughtExceptions = new ArrayList<>();
|
||||
|
||||
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||
variableInterpreter.addValueMap("input", inputValues);
|
||||
for(QFilterCriteria criterion : getCriteria())
|
||||
@ -520,45 +409,13 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
|
||||
for(Serializable value : criterion.getValues())
|
||||
{
|
||||
try
|
||||
{
|
||||
if(value instanceof AbstractFilterExpression<?>)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// if a filter variable expression, evaluate the input values, which //
|
||||
// will replace the variables with the corresponding actual values //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
if(value instanceof FilterVariableExpression filterVariableExpression)
|
||||
{
|
||||
newValues.add(filterVariableExpression.evaluateInputValues(inputValues));
|
||||
}
|
||||
else
|
||||
{
|
||||
newValues.add(value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String valueAsString = ValueUtils.getValueAsString(value);
|
||||
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
|
||||
newValues.add(interpretedValue);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
caughtExceptions.add(e);
|
||||
}
|
||||
String valueAsString = ValueUtils.getValueAsString(value);
|
||||
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
|
||||
newValues.add(interpretedValue);
|
||||
}
|
||||
criterion.setValues(newValues);
|
||||
}
|
||||
}
|
||||
|
||||
if(!caughtExceptions.isEmpty())
|
||||
{
|
||||
String message = "Error interpreting filter values: " + StringUtils.joinWithCommasAndAnd(caughtExceptions.stream().map(e -> e.getMessage()).toList());
|
||||
boolean allUserFacing = caughtExceptions.stream().allMatch(QUserFacingException.class::isInstance);
|
||||
throw (allUserFacing ? new QUserFacingException(message) : new QException(message));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -24,8 +24,6 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
@ -38,7 +36,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterf
|
||||
** Input data for the Query action
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface, Cloneable
|
||||
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface
|
||||
{
|
||||
private QBackendTransaction transaction;
|
||||
private QQueryFilter filter;
|
||||
@ -70,24 +68,6 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
|
||||
private boolean includeAssociations = false;
|
||||
private Collection<String> associationNamesToInclude = null;
|
||||
|
||||
private EnumSet<QueryHint> queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Information about the query that an application (or qqq service) may know and
|
||||
** want to tell the backend, that can help influence how the backend processes
|
||||
** query.
|
||||
**
|
||||
** For example, a query with potentially a large result set, for MySQL backend,
|
||||
** we may want to configure the result set to stream results rather than do its
|
||||
** default in-memory thing. See RDBMSQueryAction for usage.
|
||||
*******************************************************************************/
|
||||
public enum QueryHint
|
||||
{
|
||||
POTENTIALLY_LARGE_NUMBER_OF_RESULTS
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -110,40 +90,6 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QueryInput clone() throws CloneNotSupportedException
|
||||
{
|
||||
QueryInput clone = (QueryInput) super.clone();
|
||||
|
||||
if(fieldsToTranslatePossibleValues != null)
|
||||
{
|
||||
clone.fieldsToTranslatePossibleValues = new HashSet<>(fieldsToTranslatePossibleValues);
|
||||
}
|
||||
|
||||
if(queryJoins != null)
|
||||
{
|
||||
clone.queryJoins = new ArrayList<>(queryJoins);
|
||||
}
|
||||
|
||||
if(clone.associationNamesToInclude != null)
|
||||
{
|
||||
clone.associationNamesToInclude = new HashSet<>(associationNamesToInclude);
|
||||
}
|
||||
|
||||
if(queryHints != null)
|
||||
{
|
||||
clone.queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
clone.queryHints.addAll(queryHints);
|
||||
}
|
||||
|
||||
return (clone);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filter
|
||||
**
|
||||
@ -623,64 +569,4 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryHints
|
||||
*******************************************************************************/
|
||||
public EnumSet<QueryHint> getQueryHints()
|
||||
{
|
||||
return (this.queryHints);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for queryHints
|
||||
*******************************************************************************/
|
||||
public void setQueryHints(EnumSet<QueryHint> queryHints)
|
||||
{
|
||||
this.queryHints = queryHints;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public QueryInput withQueryHints(EnumSet<QueryHint> queryHints)
|
||||
{
|
||||
this.queryHints = queryHints;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public QueryInput withQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints == null)
|
||||
{
|
||||
this.queryHints = EnumSet.noneOf(QueryHint.class);
|
||||
}
|
||||
this.queryHints.add(queryHint);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryHints
|
||||
*******************************************************************************/
|
||||
public QueryInput withoutQueryHint(QueryHint queryHint)
|
||||
{
|
||||
if(this.queryHints != null)
|
||||
{
|
||||
this.queryHints.remove(queryHint);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,8 +22,6 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
@ -51,10 +49,6 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
** specific joinMetaData to use must be set. The joinMetaData field can also be
|
||||
** used instead of specify joinTable and baseTableOrAlias, but only for cases
|
||||
** where the baseTable is not an alias.
|
||||
**
|
||||
** The securityCriteria member, in general, is meant to be populated when a
|
||||
** JoinsContext is constructed before executing a query, and not meant to be set
|
||||
** by users.
|
||||
*******************************************************************************/
|
||||
public class QueryJoin
|
||||
{
|
||||
@ -65,30 +59,13 @@ public class QueryJoin
|
||||
private boolean select = false;
|
||||
private Type type = Type.INNER;
|
||||
|
||||
private List<QFilterCriteria> securityCriteria = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** define the types of joins - INNER, LEFT, RIGHT, or FULL.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum Type
|
||||
{
|
||||
INNER,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
FULL;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** check if a join is an OUTER type (LEFT or RIGHT).
|
||||
*******************************************************************************/
|
||||
public static boolean isOuter(Type type)
|
||||
{
|
||||
return (LEFT == type || RIGHT == type);
|
||||
}
|
||||
}
|
||||
{INNER, LEFT, RIGHT, FULL}
|
||||
|
||||
|
||||
|
||||
@ -371,66 +348,4 @@ public class QueryJoin
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for securityCriteria
|
||||
*******************************************************************************/
|
||||
public List<QFilterCriteria> getSecurityCriteria()
|
||||
{
|
||||
return (this.securityCriteria);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for securityCriteria
|
||||
*******************************************************************************/
|
||||
public void setSecurityCriteria(List<QFilterCriteria> securityCriteria)
|
||||
{
|
||||
this.securityCriteria = securityCriteria;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for securityCriteria
|
||||
*******************************************************************************/
|
||||
public QueryJoin withSecurityCriteria(List<QFilterCriteria> securityCriteria)
|
||||
{
|
||||
this.securityCriteria = securityCriteria;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for securityCriteria
|
||||
*******************************************************************************/
|
||||
public QueryJoin withSecurityCriteria(QFilterCriteria securityCriteria)
|
||||
{
|
||||
if(this.securityCriteria == null)
|
||||
{
|
||||
this.securityCriteria = new ArrayList<>();
|
||||
}
|
||||
this.securityCriteria.add(securityCriteria);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "QueryJoin{base="
|
||||
+ baseTableOrAlias + ", joinTable='"
|
||||
+ joinTable + ", joinMetaData="
|
||||
+ (joinMetaData == null ? null : joinMetaData.getName()) + ", alias='"
|
||||
+ alias + ", select="
|
||||
+ select + ", type="
|
||||
+ type + '}';
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,6 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -35,17 +33,7 @@ public abstract class AbstractFilterExpression<T extends Serializable> implement
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract T evaluate() throws QException;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T evaluateInputValues(Map<String, Serializable> inputValues) throws QException
|
||||
{
|
||||
return (T) this;
|
||||
}
|
||||
public abstract T evaluate();
|
||||
|
||||
|
||||
|
||||
|
@ -1,214 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class FilterVariableExpression extends AbstractFilterExpression<Serializable>
|
||||
{
|
||||
private String variableName;
|
||||
private String fieldName;
|
||||
private String operator;
|
||||
private int valueIndex = 0;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Serializable evaluate() throws QException
|
||||
{
|
||||
throw (new QUserFacingException("Missing variable value."));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Serializable evaluateInputValues(Map<String, Serializable> inputValues) throws QException
|
||||
{
|
||||
if(!inputValues.containsKey(variableName) || "".equals(ValueUtils.getValueAsString(inputValues.get(variableName))))
|
||||
{
|
||||
throw (new QUserFacingException("Missing value for variable: " + variableName));
|
||||
}
|
||||
return (inputValues.get(variableName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
private FilterVariableExpression(String fieldName, int valueIndex)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
this.valueIndex = valueIndex;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for valueIndex
|
||||
*******************************************************************************/
|
||||
public int getValueIndex()
|
||||
{
|
||||
return (this.valueIndex);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for valueIndex
|
||||
*******************************************************************************/
|
||||
public void setValueIndex(int valueIndex)
|
||||
{
|
||||
this.valueIndex = valueIndex;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for valueIndex
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression withValueIndex(int valueIndex)
|
||||
{
|
||||
this.valueIndex = valueIndex;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldName
|
||||
*******************************************************************************/
|
||||
public String getFieldName()
|
||||
{
|
||||
return (this.fieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fieldName
|
||||
*******************************************************************************/
|
||||
public void setFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fieldName
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression withFieldName(String fieldName)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variableName
|
||||
*******************************************************************************/
|
||||
public String getVariableName()
|
||||
{
|
||||
return (this.variableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variableName
|
||||
*******************************************************************************/
|
||||
public void setVariableName(String variableName)
|
||||
{
|
||||
this.variableName = variableName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variableName
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression withVariableName(String variableName)
|
||||
{
|
||||
this.variableName = variableName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for operator
|
||||
*******************************************************************************/
|
||||
public String getOperator()
|
||||
{
|
||||
return (this.operator);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for operator
|
||||
*******************************************************************************/
|
||||
public void setOperator(String operator)
|
||||
{
|
||||
this.operator = operator;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for operator
|
||||
*******************************************************************************/
|
||||
public FilterVariableExpression withOperator(String operator)
|
||||
{
|
||||
this.operator = operator;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -36,7 +35,7 @@ public class Now extends AbstractFilterExpression<Instant>
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Instant evaluate() throws QException
|
||||
public Instant evaluate()
|
||||
{
|
||||
return (Instant.now());
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -120,7 +119,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Instant evaluate() throws QException
|
||||
public Instant evaluate()
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... //
|
||||
|
@ -28,7 +28,6 @@ import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
@ -96,7 +95,7 @@ public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Instant evaluate() throws QException
|
||||
public Instant evaluate()
|
||||
{
|
||||
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
|
||||
|
||||
|
@ -39,9 +39,7 @@ public class ReplaceInput extends AbstractTableActionInput
|
||||
private UniqueKey key;
|
||||
private List<QRecord> records;
|
||||
private QQueryFilter filter;
|
||||
private boolean performDeletes = true;
|
||||
private boolean allowNullKeyValuesToEqual = false;
|
||||
private boolean setPrimaryKeyInInsertedRecords = false;
|
||||
private boolean performDeletes = true;
|
||||
|
||||
private boolean omitDmlAudit = false;
|
||||
|
||||
@ -241,66 +239,4 @@ public class ReplaceInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for allowNullKeyValuesToEqual
|
||||
*******************************************************************************/
|
||||
public boolean getAllowNullKeyValuesToEqual()
|
||||
{
|
||||
return (this.allowNullKeyValuesToEqual);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for allowNullKeyValuesToEqual
|
||||
*******************************************************************************/
|
||||
public void setAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual)
|
||||
{
|
||||
this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for allowNullKeyValuesToEqual
|
||||
*******************************************************************************/
|
||||
public ReplaceInput withAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual)
|
||||
{
|
||||
this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for setPrimaryKeyInInsertedRecords
|
||||
*******************************************************************************/
|
||||
public boolean getSetPrimaryKeyInInsertedRecords()
|
||||
{
|
||||
return (this.setPrimaryKeyInInsertedRecords);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for setPrimaryKeyInInsertedRecords
|
||||
*******************************************************************************/
|
||||
public void setSetPrimaryKeyInInsertedRecords(boolean setPrimaryKeyInInsertedRecords)
|
||||
{
|
||||
this.setPrimaryKeyInInsertedRecords = setPrimaryKeyInInsertedRecords;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for setPrimaryKeyInInsertedRecords
|
||||
*******************************************************************************/
|
||||
public ReplaceInput withSetPrimaryKeyInInsertedRecords(boolean setPrimaryKeyInInsertedRecords)
|
||||
{
|
||||
this.setPrimaryKeyInInsertedRecords = setPrimaryKeyInInsertedRecords;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,77 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.storage;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Input for Storage actions.
|
||||
*******************************************************************************/
|
||||
public class StorageInput extends AbstractTableActionInput
|
||||
{
|
||||
private String reference;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public StorageInput(String storageTableName)
|
||||
{
|
||||
super();
|
||||
setTableName(storageTableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reference
|
||||
*******************************************************************************/
|
||||
public String getReference()
|
||||
{
|
||||
return (this.reference);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reference
|
||||
*******************************************************************************/
|
||||
public void setReference(String reference)
|
||||
{
|
||||
this.reference = reference;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for reference
|
||||
*******************************************************************************/
|
||||
public StorageInput withReference(String reference)
|
||||
{
|
||||
this.reference = reference;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -120,7 +120,7 @@ public class UpdateInput extends AbstractTableActionInput
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public UpdateInput withRecordEntities(List<? extends QRecordEntity> recordEntityList)
|
||||
public UpdateInput withRecordEntities(List<QRecordEntity> recordEntityList)
|
||||
{
|
||||
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
|
||||
{
|
||||
|
@ -1,139 +0,0 @@
|
||||
/*
|
||||
* 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.core.model.dashboard.widgets;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Model containing datastructure expected by frontend alert widget
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class AlertData extends QWidgetData
|
||||
{
|
||||
public enum AlertType
|
||||
{
|
||||
ERROR,
|
||||
SUCCESS,
|
||||
WARNING
|
||||
}
|
||||
|
||||
|
||||
|
||||
private String html;
|
||||
private AlertType alertType;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AlertData()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AlertData(AlertType alertType, String html)
|
||||
{
|
||||
setHtml(html);
|
||||
setAlertType(alertType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for type
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getType()
|
||||
{
|
||||
return WidgetType.ALERT.getType();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for html
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getHtml()
|
||||
{
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for html
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setHtml(String html)
|
||||
{
|
||||
this.html = html;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for html
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AlertData withHtml(String html)
|
||||
{
|
||||
this.html = html;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for alertType
|
||||
*******************************************************************************/
|
||||
public AlertType getAlertType()
|
||||
{
|
||||
return (this.alertType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for alertType
|
||||
*******************************************************************************/
|
||||
public void setAlertType(AlertType alertType)
|
||||
{
|
||||
this.alertType = alertType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for alertType
|
||||
*******************************************************************************/
|
||||
public AlertData withAlertType(AlertType alertType)
|
||||
{
|
||||
this.alertType = alertType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -54,7 +54,6 @@ public class CompositeWidgetData extends AbstractBlockWidgetData<CompositeWidget
|
||||
/////////////////////////////////////////////////////////////
|
||||
// note, these are used in QQQ FMD CompositeWidgetData.tsx //
|
||||
/////////////////////////////////////////////////////////////
|
||||
FLEX_COLUMN,
|
||||
FLEX_ROW_WRAPPED,
|
||||
FLEX_ROW_SPACE_BETWEEN,
|
||||
TABLE_SUB_ROW_DETAILS,
|
||||
|
@ -1,188 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class DynamicFormWidgetData extends QWidgetData
|
||||
{
|
||||
private List<QFieldMetaData> fieldList;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// values for the fields - //
|
||||
// use a QRecord, so we can do "richer" things, like DisplayValues //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
private QRecord recordOfFieldValues;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
// if there are no fields, what message to display //
|
||||
/////////////////////////////////////////////////////
|
||||
private String noFieldsMessage;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// what 1 field do we want to combine the dynamic fields into (as a JSON string) //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
private String mergedDynamicFormValuesIntoFieldName;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getType()
|
||||
{
|
||||
return WidgetType.DYNAMIC_FORM.getType();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldList
|
||||
*******************************************************************************/
|
||||
public List<QFieldMetaData> getFieldList()
|
||||
{
|
||||
return (this.fieldList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for fieldList
|
||||
*******************************************************************************/
|
||||
public void setFieldList(List<QFieldMetaData> fieldList)
|
||||
{
|
||||
this.fieldList = fieldList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for fieldList
|
||||
*******************************************************************************/
|
||||
public DynamicFormWidgetData withFieldList(List<QFieldMetaData> fieldList)
|
||||
{
|
||||
this.fieldList = fieldList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for noFieldsMessage
|
||||
*******************************************************************************/
|
||||
public String getNoFieldsMessage()
|
||||
{
|
||||
return (this.noFieldsMessage);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for noFieldsMessage
|
||||
*******************************************************************************/
|
||||
public void setNoFieldsMessage(String noFieldsMessage)
|
||||
{
|
||||
this.noFieldsMessage = noFieldsMessage;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for noFieldsMessage
|
||||
*******************************************************************************/
|
||||
public DynamicFormWidgetData withNoFieldsMessage(String noFieldsMessage)
|
||||
{
|
||||
this.noFieldsMessage = noFieldsMessage;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for mergedDynamicFormValuesIntoFieldName
|
||||
*******************************************************************************/
|
||||
public String getMergedDynamicFormValuesIntoFieldName()
|
||||
{
|
||||
return (this.mergedDynamicFormValuesIntoFieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for mergedDynamicFormValuesIntoFieldName
|
||||
*******************************************************************************/
|
||||
public void setMergedDynamicFormValuesIntoFieldName(String mergedDynamicFormValuesIntoFieldName)
|
||||
{
|
||||
this.mergedDynamicFormValuesIntoFieldName = mergedDynamicFormValuesIntoFieldName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for mergedDynamicFormValuesIntoFieldName
|
||||
*******************************************************************************/
|
||||
public DynamicFormWidgetData withMergedDynamicFormValuesIntoFieldName(String mergedDynamicFormValuesIntoFieldName)
|
||||
{
|
||||
this.mergedDynamicFormValuesIntoFieldName = mergedDynamicFormValuesIntoFieldName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for recordOfFieldValues
|
||||
*******************************************************************************/
|
||||
public QRecord getRecordOfFieldValues()
|
||||
{
|
||||
return (this.recordOfFieldValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for recordOfFieldValues
|
||||
*******************************************************************************/
|
||||
public void setRecordOfFieldValues(QRecord recordOfFieldValues)
|
||||
{
|
||||
this.recordOfFieldValues = recordOfFieldValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for recordOfFieldValues
|
||||
*******************************************************************************/
|
||||
public DynamicFormWidgetData withRecordOfFieldValues(QRecord recordOfFieldValues)
|
||||
{
|
||||
this.recordOfFieldValues = recordOfFieldValues;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -27,7 +27,6 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@ -148,7 +147,7 @@ public class FieldValueListData extends QWidgetData
|
||||
}
|
||||
}
|
||||
|
||||
QValueFormatter.setDisplayValuesInRecord(null, fields.stream().collect(Collectors.toMap(f -> f.getName(), f -> f)), record);
|
||||
QValueFormatter.setDisplayValuesInRecord(fields, record);
|
||||
}
|
||||
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user