mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-20 14:10:44 +00:00
Compare commits
206 Commits
snapshot-f
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
ed9bf93986 | |||
ea1022e458 | |||
ad3d9cc6d1 | |||
01b7b3fe63 | |||
30b07e7f3a | |||
6fb39899a6 | |||
3a1125f668 | |||
4dadff7fc2 | |||
7c8ef52af9 | |||
fb8257d34a | |||
d56637b9dd | |||
1ff2bc60ae | |||
11de1cd392 | |||
3914670988 | |||
6fffe3036c | |||
1eeb57f32f | |||
6f406fc42d | |||
d94dc524b5 | |||
b28a7d9a81 | |||
8c01e87499 | |||
a24033f572 | |||
80ab0b26a0 | |||
4d4405ec32 | |||
050d102efe | |||
9ec25a19f4 | |||
782a07b176 | |||
5384eb9927 | |||
52b64ffbc0 | |||
98e9d1bf57 | |||
b37e26e03b | |||
dc9c79022f | |||
f3efb341fc | |||
d0de637dee | |||
1554815fd0 | |||
94574564de | |||
bc7db31fca | |||
addcebefa5 | |||
9ff5f82a91 | |||
82e66a159a | |||
cc3f630659 | |||
3b80c5cd3f | |||
73c0f030e2 | |||
8f7d8abd9f | |||
94e6afc3e0 | |||
8bbbd02558 | |||
bd4f5f3633 | |||
38aee86ccf | |||
b1fdd866dd | |||
b802f4a900 | |||
c683794343 | |||
e86d581fe4 | |||
df4ac3ec35 | |||
e078015732 | |||
05f31d0722 | |||
9257519462 | |||
e0134450f8 | |||
fc915a6d65 | |||
d7dec3e560 | |||
40e8a85977 | |||
30ee8ce3bf | |||
0d6538593b | |||
7e6a3c528f | |||
02d068dad7 | |||
c9f921c148 | |||
2545d03f20 | |||
92b8211f20 | |||
8afbbfb4da | |||
24b1daa110 | |||
17899c3fdc | |||
605578d661 | |||
f15277f23b | |||
d480027aeb | |||
5963a706b0 | |||
c8c7051628 | |||
7015322bf3 | |||
d6edbfa06b | |||
a10992226a | |||
04103281af | |||
2088c5dab3 | |||
59a4ad7de6 | |||
7b457b4936 | |||
d1e4091eb4 | |||
753c224196 | |||
0130e34112 | |||
5ed21d1fed | |||
c5e381abdb | |||
1bffd4d46e | |||
c093c680c0 | |||
aa69f0e7d7 | |||
2fec4891d3 | |||
60ffac4646 | |||
2a97598309 | |||
1a1ebcbe02 | |||
5f72978528 | |||
3265d6d842 | |||
246984892a | |||
03b93658d5 | |||
b093ff5ece | |||
7181643abb | |||
178b902665 | |||
d6d0e910ec | |||
d09b12ca5b | |||
2c4fc6a0d4 | |||
11d33c6d06 | |||
3684101259 | |||
c054bf5ddd | |||
7155180a76 | |||
dabaafa482 | |||
a4cdbc429d | |||
03f1fc1436 | |||
d551ad71a6 | |||
891c567a8d | |||
3e604f4b6f | |||
5564e94ad7 | |||
58c15e6eaa | |||
f448cff5dd | |||
2e1bf399f9 | |||
621997efd9 | |||
87ef20aff4 | |||
12eb1804ad | |||
51945aa844 | |||
1ee4f67286 | |||
3c2a34291a | |||
949d9cd088 | |||
acc7abf5c4 | |||
5d24780c47 | |||
cf92e20315 | |||
4f0cba88e6 | |||
2b31c7957f | |||
35d6e23157 | |||
9de1f6924a | |||
865c1cae20 | |||
21b2e5ffc0 | |||
7285efe656 | |||
1a23f5f7e9 | |||
5f0fe9ce27 | |||
7edcb61b4d | |||
7b9f659afa | |||
44b75b6437 | |||
b8330bd2f0 | |||
83ba490eb9 | |||
3d9a6fa249 | |||
e47b2c9497 | |||
fdc948f96c | |||
59ca1e3a1c | |||
52d7d0e8ae | |||
d1792dde11 | |||
b0aaf61e99 | |||
66f9d1b500 | |||
dd103d323d | |||
238521aa57 | |||
8056e49e24 | |||
2bdb5911e7 | |||
9680812828 | |||
049f5a6b42 | |||
15c0d6a45a | |||
6919d9d810 | |||
cdf59e8f2b | |||
815f160a94 | |||
0aba833af3 | |||
2522bdcf1b | |||
e27ef7b835 | |||
45899400ad | |||
7d25fc7390 | |||
878f374cb5 | |||
29a54f5293 | |||
a035bbe18f | |||
edf942a01b | |||
ed6319ff53 | |||
55b4e2154c | |||
aabe9e315e | |||
217acc954a | |||
557824c572 | |||
85ac2c10e7 | |||
b506150842 | |||
3b8eef0f9c | |||
c07d9a779a | |||
4609ff8976 | |||
bf11ea5185 | |||
01d96cc8ae | |||
a6d84c66b7 | |||
69015be2f6 | |||
0f16446f69 | |||
9464a308f2 | |||
51f021193d | |||
0090601166 | |||
cf78f1cb94 | |||
0b1b570f28 | |||
73e54dd56f | |||
e936468f29 | |||
73ac3448dd | |||
d54010e89d | |||
dae23c2a04 | |||
6e3ef2254a | |||
b03de8ec0f | |||
0dd97d9dc1 | |||
b1e68017cc | |||
a37a0b489d | |||
940c5ca8de | |||
84093dfde5 | |||
9c7d94f764 | |||
346443996b | |||
2fc513891f | |||
7426aa36a5 | |||
9d8e8a74e2 | |||
455ab69104 |
@ -1,7 +1,7 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
localstack: localstack/platform@1.0
|
||||
localstack: localstack/platform@2.1
|
||||
|
||||
commands:
|
||||
store_jacoco_site:
|
||||
|
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.html
|
64
docs/actions/GetAction.adoc
Normal file
64
docs/actions/GetAction.adoc
Normal file
@ -0,0 +1,64 @@
|
||||
== GetAction
|
||||
include::../variables.adoc[]
|
||||
|
||||
The `*GetAction*` is essentially a subset of the <<QueryAction>>, only specifically meant to get just a single record from a {link-table}.
|
||||
In SQL/RDBMS terms, it is analogous to a `SELECT` statement, where a single record may be found - or - it may not be found.
|
||||
|
||||
For all tables, `GetAction` can do a lookup by primary key.
|
||||
In addition, for tables that define a `UniqueKey`, name/value pairs (in the form of a `Map`) can be used as input for `GetAction`.
|
||||
|
||||
=== Examples
|
||||
[source,java]
|
||||
.Basic form - by primary key
|
||||
----
|
||||
GetInput input = new GetInput();
|
||||
input.setTableName("orders");
|
||||
input.setPrimaryKey(1);
|
||||
GetOutput output = new GetAction.execute(input);
|
||||
QRecord record = output.getRecord();
|
||||
----
|
||||
|
||||
[source,java]
|
||||
.Secondary form - by Unique Key
|
||||
----
|
||||
GetInput input = new GetInput();
|
||||
input.setTableName("products");
|
||||
input.setUniqueKey(Map.of("storeId", 1701, "sku", "ABCD"));
|
||||
GetOutput output = new GetAction.execute(input);
|
||||
QRecord record = output.getRecord();
|
||||
----
|
||||
|
||||
=== GetInput
|
||||
* `table` - *String, Required* - Name of the table being queried against.
|
||||
* `primaryKey` - *Serializable, Conditional* - Value for the primary key field of the table being queried.
|
||||
Type should match the table's primary key field's type.
|
||||
If a `primaryKey` is not given, then a `uniqueKey` must be given.
|
||||
* `uniqueKey` - *Map of String -> Serializable, Conditional* - Map of name-value pairs that define the record to be fetcheed.
|
||||
Keys in the map must be field names from the table being queried.
|
||||
Values in the map should should be of types that correspond to the fields.
|
||||
If a `primaryKey` is not given, then a `uniqueKey` must be given.
|
||||
If both `primaryKey` and `uniqueKey` are given, then `uniqueKey` is ignored.
|
||||
* `transaction` - *QBackendTransaction object* - Optional transaction object.
|
||||
** Behavior for this object is backend-dependant.
|
||||
In an RDBMS backend, this object is generally needed if you want your query to see data that may have been modified within the same transaction.
|
||||
|
||||
* `shouldTranslatePossibleValues` - *boolean, default: false* - Controls whether any fields in the table with a *possibleValueSource* assigned to them should have those possible values looked up
|
||||
(e.g., to provide text translations in the generated records' `displayValues` map).
|
||||
** For example, if getting a record to present to a user, this would generally need to be *true*.
|
||||
But if getting a record as part of a process, then this can generally be left as *false*.
|
||||
* `shouldGenerateDisplayValues` - *boolean, default: false* - Controls whether field level *displayFormats* should be used to populate the generated records' `displayValues` map.
|
||||
** For example, if getting a record to present to a user, this would generally need to be *true*.
|
||||
But if getting a record as part of a process, then this can generally be left as *false*.
|
||||
* `shouldFetchHeavyFields` - *boolean, default: true* - Controls whether or not fields marked as `isHeavy` should be fetched & returned or not.
|
||||
* `shouldOmitHiddenFields` - *boolean, default: true* - Controls whether or not fields marked as `isHidden` should be included in the result or not.
|
||||
* `shouldMaskPassword` - *boolean, default: true* - Controls whether or not fields with `type` = `PASSWORD` should be masked, or if their actual values should be returned.
|
||||
* `queryJoins` - *List of <<QueryJoin>> objects* - Optional list of tables to be joined with the main table being queried.
|
||||
See QueryJoin under <<QueryAction>> for further details.
|
||||
* `includeAssociations` - *boolean, default: false* - Control whether or not records from tables defined as `associations` under the table being queried should be included in the result or not.
|
||||
* `associationNamesToInclude` - *Collection of String* - If `includeAssociations` is true, then this field can be used to limit which associated tables are included.
|
||||
If this field is null, then all associated tables are included.
|
||||
Otherwise, a table is only included if its name is in this collection.
|
||||
|
||||
=== GetOutput
|
||||
* `record` - *QRecord* - The record that was specified by the input `primaryKey` or `uniqueKey`.
|
||||
Will be null if the record was not found.
|
86
docs/actions/InsertAction.adoc
Normal file
86
docs/actions/InsertAction.adoc
Normal file
@ -0,0 +1,86 @@
|
||||
== InsertAction
|
||||
include::../variables.adoc[]
|
||||
|
||||
To insert (add, create) new records into any {link-table}, the `*InsertAction*` is used.
|
||||
In SQL/RDBMS terms, it is analogous to a `INSERT` statement, where one or more records can be provided as input.
|
||||
|
||||
=== Examples
|
||||
[source,java]
|
||||
.Canonical InsertAction invocation
|
||||
----
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName("person");
|
||||
insertInput.setRecords(personRecordList);
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
List<QRecord> insertedPersonRecords = insertOutput.getRecords();
|
||||
----
|
||||
|
||||
=== Details
|
||||
`InsertAction` does several things beyond just inserting records into the specified table.
|
||||
A high-level flow of its internal logic is:
|
||||
|
||||
. For tables using an automation status field, set its value to `PENDING_INSERT_AUTOMATIONS` for all `records` that are going to be inserted.
|
||||
. Perform the following validations, which include running the table's `PRE_INSERT_CUSTOMIZER`, if one is defined, at the time that is specified by the customizer's `WhenToRun` property (default is `AFTER_ALL_VALIDATIONS`):
|
||||
.. Ensure that default values specified in the table's fields are present if needed.
|
||||
.. Apply per-field behaviors, as defined in {link-field} meta-data, such as truncating strings that are longer than their specified max.
|
||||
.. Check for unique key violations (done here instead of in the backend, to provide better error messaging, and to allow a subset of records to be stored while some fail).
|
||||
_We might want to make an input control in the future to specify that either the full input set should succeed or fail..._
|
||||
.. Validate that required fields (again, per {link-field} meta-data) are set, generating per-record errors if they are not.
|
||||
.. Validate any security fields in the records - e.g., ensure that the user has permission to insert records with the values they are attempting to insert.
|
||||
. Send the records to the table's backend module to actually insert them into the backend storage.
|
||||
. If the table has any associations defined, and if associated records are present, then recursively run `InsertAction` on the associated records.
|
||||
.. In particular, before these recursive `InsertAction` calls are made, values that were generated by the original insert may need to be propagated down into the associated records.
|
||||
*** For example, if inserting `order` and `lineItem` records, where a {link-join} exists between the two tables on `order.id` and `lineItem.orderId`, and `order.id` values were generated in the first `InsertAction`, then those values are propagated down into the associated `lineItem.orderId` fields.
|
||||
. If the {link-instance} has an `audit` table, then based on the {link-table}'s audit rules, audits about the inserted records are created.
|
||||
. If the table has a `POST_INSERT_CUSTOMIZER`, it is executed.
|
||||
|
||||
=== Overloads
|
||||
|
||||
`InsertAction` can be called in a few alternate forms, mostly just for convenience:
|
||||
|
||||
[source,java]
|
||||
.If inserting a single record, get that record back instead of the InsertOutput:
|
||||
----
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName("person");
|
||||
insertInput.setRecords(List.of(personRecord));
|
||||
QRecord insertedRecord = new InsertAction().executeForRecord(insertInput);
|
||||
|
||||
// or more compactly, using InsertInput.withRecord (instead of withRecords)
|
||||
QRecord insertedRecord = new InsertAction()
|
||||
.executeForRecord(new InsertInput("person").withRecord(personRecord));
|
||||
----
|
||||
|
||||
[source,java]
|
||||
.Taking QRecordEntity objects as inputs instead of QRecords:
|
||||
----
|
||||
// insert a list of person entities:
|
||||
InsertInput insertInput = new InsertInput("person").withRecordEntities(personList);
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
|
||||
// or for a single person entity (also mapping the output record back to an entity):
|
||||
Person insertedPerson = new Person(new InsertAction()
|
||||
.executeForRecord(new InsertInput("person").withRecordEntity(person)));
|
||||
|
||||
----
|
||||
|
||||
=== InsertInput
|
||||
* `table` - *String, Required* - Name of the table that records are to be inserted into.
|
||||
* `records` - *List of QRecord, Required* - List of records to be inserted into the table.
|
||||
If the list is empty, the insert action does a `noop`.
|
||||
|
||||
.Less common options
|
||||
* `inputSource` - *InputSource object, default: QInputSource.SYSTEM* - an indicator of what the source of the action is - generally, a `SYSTEM` triggered action, or a `USER` triggered action.
|
||||
** `InsertAction` will call the `shouldValidateRequiredFields()` method on this object to determine if it should validate that required fields on the records have values.
|
||||
Both `QInputSource.SYSTEM` and `QInputSource.USER` return `true` from this method, but application can define their own `InputSource` objects with different behavior.
|
||||
** In addition, this field can be used in pre- and post-insert customizers to drive further custom logic.
|
||||
* `skipUniqueKeyCheck` - *boolean, default: false* - control whether or not `InsertAction` should check for unique key violations before attempting to insert its records.
|
||||
** In a context where one has previously done this validation, or is okay letting the backend provide such checks, they may wish to avoid re-doing this work, and thus may set this property to `true`.
|
||||
* `omitDmlAudit` - *boolean, default: false* - control if the automatic DML audit that `InsertAction` generally performs should be omitted.
|
||||
* `auditContext` - *String* - optional message which can be included in DML audit messages, to give users more context about why the insert occurred.
|
||||
|
||||
|
||||
=== InsertOutput
|
||||
* `records` - *List of QRecord* - Copy of the input list of records, with details added based on the results of the input action.
|
||||
** If there were warnings or errors, the corresponding field (`warnings` or `errors`) will be set in the records.
|
||||
** If the insert action generated any values (such as a serial id or a default value), those values will be in the record's `fields` map.
|
@ -26,12 +26,22 @@ include::metaData/Reports.adoc[leveloffset=+1]
|
||||
include::metaData/Icons.adoc[leveloffset=+1]
|
||||
include::metaData/PermissionRules.adoc[leveloffset=+1]
|
||||
|
||||
== Services
|
||||
|
||||
include::misc/ScheduledJobs.adoc[leveloffset=+1]
|
||||
|
||||
=== Web server (Javalin)
|
||||
#todo#
|
||||
|
||||
=== API server (OpenAPI)
|
||||
#todo#
|
||||
|
||||
== Custom Application Code
|
||||
include::misc/QContext.adoc[leveloffset=+1]
|
||||
include::misc/QRecords.adoc[leveloffset=+1]
|
||||
include::misc/QRecordEntities.adoc[leveloffset=+1]
|
||||
include::misc/ProcessBackendSteps.adoc[leveloffset=+1]
|
||||
include::misc/RenderingWidgets.adoc[leveloffset=+1]
|
||||
|
||||
=== Table Customizers
|
||||
#todo#
|
||||
@ -61,3 +71,6 @@ include::actions/InsertAction.adoc[leveloffset=+1]
|
||||
== QQQ Default Implementations
|
||||
include::implementations/TableSync.adoc[leveloffset=+1]
|
||||
// later... include::actions/RenderTemplateAction.adoc[leveloffset=+1]
|
||||
|
||||
== QQQ Utility Classes
|
||||
include::utilities/RecordLookupHelper.adoc[leveloffset=+1]
|
||||
|
12
docs/justfile
Normal file
12
docs/justfile
Normal file
@ -0,0 +1,12 @@
|
||||
## https://github.com/casey/just
|
||||
|
||||
default:
|
||||
just --list
|
||||
|
||||
build-index-html:
|
||||
asciidoctor -a docinfo=shared index.adoc
|
||||
line=$(grep 'Last updated' index.html) && sed -i "s/id=\"content\">/&$line/" index.html
|
||||
|
||||
build-and-publish-index-html: build-index-html
|
||||
scp index.html first-node:/mnt/first-volume/dkelkhoff/nginx/html/justinsgotskinnylegs.com/qqq-docs.html
|
||||
@echo "Updated: https://justinsgotskinnylegs.com/qqq-docs.html"
|
20
docs/metaData/Icons.adoc
Normal file
20
docs/metaData/Icons.adoc
Normal file
@ -0,0 +1,20 @@
|
||||
[#Icons]
|
||||
== Icons
|
||||
include::../variables.adoc[]
|
||||
|
||||
#TODO#
|
||||
|
||||
=== QIcon
|
||||
Icons are defined in a QQQ Instance in a `*QIcon*` object.
|
||||
|
||||
#TODO#
|
||||
|
||||
*QIcon Properties:*
|
||||
|
||||
* `name` - *String* - Name of an icon from the https://mui.com/material-ui/material-icons/[Material UI Icon set]
|
||||
** Note that icon names from the link above need to be converted from _CamelCase_ to _underscore_case_...
|
||||
* `path` - *String* - Path to a file served under the application's web root, to be used as the icon.
|
||||
|
||||
_Either `name` or `path` must be specified. If both are given, then name is used._
|
||||
|
||||
|
13
docs/metaData/PermissionRules.adoc
Normal file
13
docs/metaData/PermissionRules.adoc
Normal file
@ -0,0 +1,13 @@
|
||||
[#PermissionRules]
|
||||
== Permission Rules
|
||||
include::../variables.adoc[]
|
||||
|
||||
#TODO#
|
||||
|
||||
=== PermissionRule
|
||||
#TODO#
|
||||
|
||||
*PermissionRule Properties:*
|
||||
|
||||
#TODO#
|
||||
|
39
docs/metaData/QInstance.adoc
Normal file
39
docs/metaData/QInstance.adoc
Normal file
@ -0,0 +1,39 @@
|
||||
[#QInstance]
|
||||
== QInstance
|
||||
include::../variables.adoc[]
|
||||
|
||||
An application in QQQ is defined as a set of Meta Data objects. These objects are all stored together in an object called a `*QInstance*`.
|
||||
|
||||
Currently, a `QInstance` must be programmatically constructed in java code - e.g., by constructing objects which get added to the QInstance, for example:
|
||||
|
||||
[source,java]
|
||||
.Adding meta-data for two tables and one process to a QInstance
|
||||
----
|
||||
QInstance qInstance = new QInstance();
|
||||
qInstance.addTable(definePersonTable());
|
||||
qInstance.addTable(defineHomeTable());
|
||||
qInstance.addProcess(defineSendPersonHomeProcess());
|
||||
----
|
||||
|
||||
It is on the QQQ roadmap to allow meta-data to be defined in a non-programmatic way, for example, in YAML or JSON files, or even from a dynamic data source (e.g. a document or relational database).
|
||||
|
||||
The middleware and/or frontends being used in an application will drive how the `QInstance` is connected to the running server/process.
|
||||
For example, using the `qqq-middleware-javalin` module, a the `QJavalinImplementation` class () has a constructor which takes a `QInstance` as an argument:
|
||||
|
||||
[source,java]
|
||||
.Starting a QQQ Javalin middleware server - passing a QInstance as a parameter to the new QJavalinImplementation
|
||||
----
|
||||
QJavalinImplementation qJavalinImplementation = new QJavalinImplementation(qInstance);
|
||||
Javalin service = Javalin.create();
|
||||
service.routes(qJavalinImplementation.getRoutes());
|
||||
service.start();
|
||||
----
|
||||
|
||||
*QBackendMetaData Setup Methods:*
|
||||
|
||||
These are the methods that one is most likely to use when setting up (defining) a `QInstance` object:
|
||||
|
||||
* asdf
|
||||
|
||||
*QBackendMetaData Usage Methods:*
|
||||
|
@ -2,16 +2,79 @@
|
||||
== Security Key Types
|
||||
include::../variables.adoc[]
|
||||
|
||||
#TODO#
|
||||
In QQQ, record-level security is provided by using a lock & key metaphor.
|
||||
|
||||
The use-case being handled here is:
|
||||
|
||||
* A user has full permission on a table (query, insert, update, and delete).
|
||||
* However, they should only be allowed to read a sub-set of the rows in the table.
|
||||
** e.g., maybe it's a multi-tenant system, or the table has user-specific records.
|
||||
|
||||
The lock & key metaphor is realized by the user being associated with one or more "Keys"
|
||||
(as values in their session), and records in tables being associated with one or more "Locks"
|
||||
(as values in fields).
|
||||
A user is only allowed to access records where the user's key(s) match the record's security lock(s).
|
||||
|
||||
For a practical example, picture a multi-tenant Order Management System,where all orders are assigned to a "client".
|
||||
Users (customers) should only be able to see orders associated with the client which that user works for.
|
||||
|
||||
In this scenario, the `order` table would have a "lock" on its `clientId` field.
|
||||
Customer-users would have a `clientId` key in their session.
|
||||
When the QQQ backend did a search for records (e.g., an SQL query) it would implicitly
|
||||
(without any code being written by the application developer) filter the table to only
|
||||
allow the user to see records with their `clientId`.
|
||||
|
||||
To implement this scenario, the application would define the following pieces of meta-data:
|
||||
|
||||
* At the QQQ-Instance level, a `SecurityKeyType`,
|
||||
to define a domain of possible locks & keys within an application.
|
||||
** An application can define multiple Security Key Types.
|
||||
For example, maybe `clientId` and `userId` as key types.
|
||||
* At the per-table level, a `RecordSecurityLock`,
|
||||
which references a security key type, and how that key type should be applied to the table.
|
||||
** For example, what field stores the `clientId` value in the `order` table.
|
||||
* Finally, when a user's session is constructed via a QQQ Authentication provider,
|
||||
security key values are set, based on data from the authentication backend.
|
||||
|
||||
=== Additional Scenarios
|
||||
|
||||
==== All Access Key
|
||||
A "super-user" may be allowed to access all records in a table regardless of their record locks,
|
||||
if the Security Key Type specifies an `allAccessKeyName`,
|
||||
and if the user has a key in their session with that key name, and a value of `true`.
|
||||
Going back to the lock & key metaphor, this can be thought of as a "skeleton key",
|
||||
that can unlock any record lock (of the security key's type).
|
||||
|
||||
==== Null Value Behaviors
|
||||
In a record security lock, different behaviors can be defined for handling rows with a null key value.
|
||||
|
||||
For example:
|
||||
|
||||
* Sometimes orders may be loaded into the OMS system described above, where the application doesn't yet know what client the order belongs to.
|
||||
In this case, the application may need to ensure that such records, with a `null` value in `clientId` are hidden from customer-users,
|
||||
to avoid potentially leaking a different client's data.
|
||||
** This can be accomplished with a record security lock on the `order` table, with a `nullValueBehavior` of `DENY`.
|
||||
** Furthermore, if internal/admin users _should_ be given access to such records, then the security key type can be
|
||||
configured with a `nullValueBehaviorKeyName` (e.g., `"clientIdNullValueBehavior"`), which can be set per-user to allow
|
||||
access to records, overriding the table lock's specified `nullValueBehavior`.
|
||||
*** This could also be done by giving internal/admin users an `allAccessKey`, but sometimes that is not what is required.
|
||||
|
||||
* Maybe a warehouse locations table is assigned a `clientId` once inventory for a client is placed in the location,
|
||||
at which point in time, only the client's users should be allowed to see the record.
|
||||
But, if no client has been assigned to the location, and `clientId` is `null`,
|
||||
then you may want to allow any user to see such records.
|
||||
** This can be accomplished with a record security lock on the Warehouse Locations table, with a `nullValueBehavior` of `ALLOW`.
|
||||
|
||||
=== QSecurityKeyType
|
||||
A Security Key Type is defined in a QQQ Instance in a `*QSecurityKeyType*` object.
|
||||
|
||||
#TODO#
|
||||
|
||||
*QSecurityKeyType Properties:*
|
||||
|
||||
* `name` - *String, Required* - Unique name for the security key type within the QQQ Instance.
|
||||
* `name` - *String, Required* - Unique name for this security key type within the QQQ Instance.
|
||||
* `allAccessKeyName` - *String* - Optional name of the all-access security key associated with this key type.
|
||||
* `nullValueBehaviorKeyName` - *String* - Optional name of the null-value-behavior overriding security key associated with this key type.
|
||||
** Note, `name`, `allAccessKeyName`, and `nullValueBehaviorKeyName` are all checked against each other for uniqueness.
|
||||
A `QInstanceValidationException` will be thrown if any name collisions occur.
|
||||
* `possibleValueSourceName` - *String* - Optional reference to a possible value source from which value for the key can come.
|
||||
|
||||
#TODO#
|
||||
|
||||
|
@ -244,3 +244,41 @@ QQQ provides the mechanism for UI's to present and manage such scripts (e.g., th
|
||||
* `scriptTypeId` - *Serializable (typically Integer), Required* - primary key value from the `"scriptType"` table in the instance, to designate the type of the Script.
|
||||
* `scriptTester` - *QCodeReference* - reference to a class which implements `TestScriptActionInterface`, that can be used by UI's for running an associated script to test it.
|
||||
|
||||
|
||||
|
||||
=== Supplemental Meta Data
|
||||
==== QQQ Frontend Material Dashboard
|
||||
When running a QQQ application with the QQQ Frontend Material Dashboard module (QFMD),
|
||||
there are various pieces of supplemental meta-data which can be assigned to a Table,
|
||||
to modify some behaviors for the table in this UI.
|
||||
|
||||
===== Default Quick Filter Field Names
|
||||
QFMD's table query has a "Basic" mode, which will always display a subset of the table's fields as quick-access filters.
|
||||
By default, the "Tier 1" fields on a table (e.g., fields in a Section that is marked as T1) will be used for this purpose.
|
||||
|
||||
However, you can customize which fields are shown as the default quick-filter fields, by providing a list of field names in a
|
||||
`MaterialDashboardTableMetaData` object, placed in the table's `supplementalMetaData`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
table.withSupplementalMetaData(new MaterialDashboardTableMetaData()
|
||||
.withDefaultQuickFilterFieldNames(List.of("id", "warehouseId", "statusId", "orderDate")));
|
||||
----
|
||||
|
||||
===== Go To Field Names
|
||||
QFMD has a feature where a table's query screen can include a "Go To" button,
|
||||
which a user can hit to open a modal popup, into which the user can enter a record's identifier,
|
||||
to be brought directly to the record matching that identifier.
|
||||
|
||||
To use this feature, the table must have a List of `GotoFieldNames` set in its
|
||||
`MaterialDashboardTableMetaData` object in the table's `supplementalMetaData`.
|
||||
|
||||
Each entry in this list is actually a list of fields, e.g., to account for a multi-value unique-key.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
table.withSupplementalMetaData(new MaterialDashboardTableMetaData()
|
||||
.withGotoFieldNames(List.of(
|
||||
List.of("id"),
|
||||
List.of("partnerName", "partnerOrderId"))));
|
||||
----
|
||||
|
@ -1,553 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="generator" content="Asciidoctor 2.0.18">
|
||||
<title>QQQ Tables</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
|
||||
<style>
|
||||
/*! Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */
|
||||
/* Uncomment the following line when using as a custom stylesheet */
|
||||
/* @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; */
|
||||
html{font-family:sans-serif;-webkit-text-size-adjust:100%}
|
||||
a{background:none}
|
||||
a:focus{outline:thin dotted}
|
||||
a:active,a:hover{outline:0}
|
||||
h1{font-size:2em;margin:.67em 0}
|
||||
b,strong{font-weight:bold}
|
||||
abbr{font-size:.9em}
|
||||
abbr[title]{cursor:help;border-bottom:1px dotted #dddddf;text-decoration:none}
|
||||
dfn{font-style:italic}
|
||||
hr{height:0}
|
||||
mark{background:#ff0;color:#000}
|
||||
code,kbd,pre,samp{font-family:monospace;font-size:1em}
|
||||
pre{white-space:pre-wrap}
|
||||
q{quotes:"\201C" "\201D" "\2018" "\2019"}
|
||||
small{font-size:80%}
|
||||
sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||
sup{top:-.5em}
|
||||
sub{bottom:-.25em}
|
||||
img{border:0}
|
||||
svg:not(:root){overflow:hidden}
|
||||
figure{margin:0}
|
||||
audio,video{display:inline-block}
|
||||
audio:not([controls]){display:none;height:0}
|
||||
fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
|
||||
legend{border:0;padding:0}
|
||||
button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
|
||||
button,input{line-height:normal}
|
||||
button,select{text-transform:none}
|
||||
button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}
|
||||
button[disabled],html input[disabled]{cursor:default}
|
||||
input[type=checkbox],input[type=radio]{padding:0}
|
||||
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
|
||||
textarea{overflow:auto;vertical-align:top}
|
||||
table{border-collapse:collapse;border-spacing:0}
|
||||
*,::before,::after{box-sizing:border-box}
|
||||
html,body{font-size:100%}
|
||||
body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;line-height:1;position:relative;cursor:auto;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-wrap:anywhere;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}
|
||||
a:hover{cursor:pointer}
|
||||
img,object,embed{max-width:100%;height:auto}
|
||||
object,embed{height:100%}
|
||||
img{-ms-interpolation-mode:bicubic}
|
||||
.left{float:left!important}
|
||||
.right{float:right!important}
|
||||
.text-left{text-align:left!important}
|
||||
.text-right{text-align:right!important}
|
||||
.text-center{text-align:center!important}
|
||||
.text-justify{text-align:justify!important}
|
||||
.hide{display:none}
|
||||
img,object,svg{display:inline-block;vertical-align:middle}
|
||||
textarea{height:auto;min-height:50px}
|
||||
select{width:100%}
|
||||
.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
|
||||
div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0}
|
||||
a{color:#2156a5;text-decoration:underline;line-height:inherit}
|
||||
a:hover,a:focus{color:#1d4b8f}
|
||||
a img{border:0}
|
||||
p{line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
|
||||
p aside{font-size:.875em;line-height:1.35;font-style:italic}
|
||||
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
|
||||
h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
|
||||
h1{font-size:2.125em}
|
||||
h2{font-size:1.6875em}
|
||||
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
|
||||
h4,h5{font-size:1.125em}
|
||||
h6{font-size:1em}
|
||||
hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em}
|
||||
em,i{font-style:italic;line-height:inherit}
|
||||
strong,b{font-weight:bold;line-height:inherit}
|
||||
small{font-size:60%;line-height:inherit}
|
||||
code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
|
||||
ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
|
||||
ul,ol{margin-left:1.5em}
|
||||
ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0}
|
||||
ul.circle{list-style-type:circle}
|
||||
ul.disc{list-style-type:disc}
|
||||
ul.square{list-style-type:square}
|
||||
ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit}
|
||||
ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
|
||||
dl dt{margin-bottom:.3125em;font-weight:bold}
|
||||
dl dd{margin-bottom:1.25em}
|
||||
blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
|
||||
blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
|
||||
@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
|
||||
h1{font-size:2.75em}
|
||||
h2{font-size:2.3125em}
|
||||
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
|
||||
h4{font-size:1.4375em}}
|
||||
table{background:#fff;margin-bottom:1.25em;border:1px solid #dedede;word-wrap:normal}
|
||||
table thead,table tfoot{background:#f7f8f7}
|
||||
table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
|
||||
table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
|
||||
table tr.even,table tr.alt{background:#f8f8f7}
|
||||
table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{line-height:1.6}
|
||||
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
|
||||
h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
|
||||
.center{margin-left:auto;margin-right:auto}
|
||||
.stretch{width:100%}
|
||||
.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table}
|
||||
.clearfix::after,.float-group::after{clear:both}
|
||||
:not(pre).nobreak{word-wrap:normal}
|
||||
:not(pre).nowrap{white-space:nowrap}
|
||||
:not(pre).pre-wrap{white-space:pre-wrap}
|
||||
:not(pre):not([class^=L])>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background:#f7f7f8;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed}
|
||||
pre{color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;line-height:1.45;text-rendering:optimizeSpeed}
|
||||
pre code,pre pre{color:inherit;font-size:inherit;line-height:inherit}
|
||||
pre>code{display:block}
|
||||
pre.nowrap,pre.nowrap pre{white-space:pre;word-wrap:normal}
|
||||
em em{font-style:normal}
|
||||
strong strong{font-weight:400}
|
||||
.keyseq{color:rgba(51,51,51,.8)}
|
||||
kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background:#f7f7f7;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 0 rgba(0,0,0,.2),inset 0 0 0 .1em #fff;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}
|
||||
.keyseq kbd:first-child{margin-left:0}
|
||||
.keyseq kbd:last-child{margin-right:0}
|
||||
.menuseq,.menuref{color:#000}
|
||||
.menuseq b:not(.caret),.menuref{font-weight:inherit}
|
||||
.menuseq{word-spacing:-.02em}
|
||||
.menuseq b.caret{font-size:1.25em;line-height:.8}
|
||||
.menuseq i.caret{font-weight:bold;text-align:center;width:.45em}
|
||||
b.button::before,b.button::after{position:relative;top:-1px;font-weight:400}
|
||||
b.button::before{content:"[";padding:0 3px 0 2px}
|
||||
b.button::after{content:"]";padding:0 2px 0 3px}
|
||||
p a>code:hover{color:rgba(0,0,0,.9)}
|
||||
#header,#content,#footnotes,#footer{width:100%;margin:0 auto;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
|
||||
#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table}
|
||||
#header::after,#content::after,#footnotes::after,#footer::after{clear:both}
|
||||
#content{margin-top:1.25em}
|
||||
#content::before{content:none}
|
||||
#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
|
||||
#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf}
|
||||
#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px}
|
||||
#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:flex;flex-flow:row wrap}
|
||||
#header .details span:first-child{margin-left:-.125em}
|
||||
#header .details span.email a{color:rgba(0,0,0,.85)}
|
||||
#header .details br{display:none}
|
||||
#header .details br+span::before{content:"\00a0\2013\00a0"}
|
||||
#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
|
||||
#header .details br+span#revremark::before{content:"\00a0|\00a0"}
|
||||
#header #revnumber{text-transform:capitalize}
|
||||
#header #revnumber::after{content:"\00a0"}
|
||||
#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
|
||||
#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em}
|
||||
#toc>ul{margin-left:.125em}
|
||||
#toc ul.sectlevel0>li>a{font-style:italic}
|
||||
#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
|
||||
#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
|
||||
#toc li{line-height:1.3334;margin-top:.3334em}
|
||||
#toc a{text-decoration:none}
|
||||
#toc a:active{text-decoration:underline}
|
||||
#toctitle{color:#7a2518;font-size:1.2em}
|
||||
@media screen and (min-width:768px){#toctitle{font-size:1.375em}
|
||||
body.toc2{padding-left:15em;padding-right:0}
|
||||
#toc.toc2{margin-top:0!important;background:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
|
||||
#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}
|
||||
#toc.toc2>ul{font-size:.9em;margin-bottom:0}
|
||||
#toc.toc2 ul ul{margin-left:0;padding-left:1em}
|
||||
#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
|
||||
body.toc2.toc-right{padding-left:0;padding-right:15em}
|
||||
body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}}
|
||||
@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
|
||||
#toc.toc2{width:20em}
|
||||
#toc.toc2 #toctitle{font-size:1.375em}
|
||||
#toc.toc2>ul{font-size:.95em}
|
||||
#toc.toc2 ul ul{padding-left:1.25em}
|
||||
body.toc2.toc-right{padding-left:0;padding-right:20em}}
|
||||
#content #toc{border:1px solid #e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;border-radius:4px}
|
||||
#content #toc>:first-child{margin-top:0}
|
||||
#content #toc>:last-child{margin-bottom:0}
|
||||
#footer{max-width:none;background:rgba(0,0,0,.8);padding:1.25em}
|
||||
#footer-text{color:hsla(0,0%,100%,.8);line-height:1.44}
|
||||
#content{margin-bottom:.625em}
|
||||
.sect1{padding-bottom:.625em}
|
||||
@media screen and (min-width:768px){#content{margin-bottom:1.25em}
|
||||
.sect1{padding-bottom:1.25em}}
|
||||
.sect1:last-child{padding-bottom:0}
|
||||
.sect1+.sect1{border-top:1px solid #e7e7e9}
|
||||
#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
|
||||
#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
|
||||
#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
|
||||
#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
|
||||
#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
|
||||
details,.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
|
||||
details{margin-left:1.25rem}
|
||||
details>summary{cursor:pointer;display:block;position:relative;line-height:1.6;margin-bottom:.625rem;outline:none;-webkit-tap-highlight-color:transparent}
|
||||
details>summary::-webkit-details-marker{display:none}
|
||||
details>summary::before{content:"";border:solid transparent;border-left:solid;border-width:.3em 0 .3em .5em;position:absolute;top:.5em;left:-1.25rem;transform:translateX(15%)}
|
||||
details[open]>summary::before{border:solid transparent;border-top:solid;border-width:.5em .3em 0;transform:translateY(15%)}
|
||||
details>summary::after{content:"";width:1.25rem;height:1em;position:absolute;top:.3em;left:-1.25rem}
|
||||
.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
|
||||
table.tableblock.fit-content>caption.title{white-space:nowrap;width:0}
|
||||
.paragraph.lead>p,#preamble>.sectionbody>[class=paragraph]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)}
|
||||
.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
|
||||
.admonitionblock>table td.icon{text-align:center;width:80px}
|
||||
.admonitionblock>table td.icon img{max-width:none}
|
||||
.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
|
||||
.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere}
|
||||
.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
|
||||
.exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px}
|
||||
.exampleblock>.content>:first-child{margin-top:0}
|
||||
.exampleblock>.content>:last-child{margin-bottom:0}
|
||||
.sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px}
|
||||
.sidebarblock>:first-child{margin-top:0}
|
||||
.sidebarblock>:last-child{margin-bottom:0}
|
||||
.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
|
||||
.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
|
||||
.literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em}
|
||||
@media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}}
|
||||
@media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}}
|
||||
.literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#f7f7f8}
|
||||
.literalblock.output pre{color:#f7f7f8;background:rgba(0,0,0,.9)}
|
||||
.listingblock>.content{position:relative}
|
||||
.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:inherit;opacity:.5}
|
||||
.listingblock:hover code[data-lang]::before{display:block}
|
||||
.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:inherit;opacity:.5}
|
||||
.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"}
|
||||
.listingblock pre.highlightjs{padding:0}
|
||||
.listingblock pre.highlightjs>code{padding:1em;border-radius:4px}
|
||||
.listingblock pre.prettyprint{border-width:0}
|
||||
.prettyprint{background:#f7f7f8}
|
||||
pre.prettyprint .linenums{line-height:1.45;margin-left:2em}
|
||||
pre.prettyprint li{background:none;list-style-type:inherit;padding-left:0}
|
||||
pre.prettyprint li code[data-lang]::before{opacity:1}
|
||||
pre.prettyprint li:not(:first-child) code[data-lang]::before{display:none}
|
||||
table.linenotable{border-collapse:separate;border:0;margin-bottom:0;background:none}
|
||||
table.linenotable td[class]{color:inherit;vertical-align:top;padding:0;line-height:inherit;white-space:normal}
|
||||
table.linenotable td.code{padding-left:.75em}
|
||||
table.linenotable td.linenos,pre.pygments .linenos{border-right:1px solid;opacity:.35;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||
pre.pygments span.linenos{display:inline-block;margin-right:.75em}
|
||||
.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
|
||||
.quoteblock:not(.excerpt)>.title{margin-left:-1.5em;margin-bottom:.75em}
|
||||
.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
|
||||
.quoteblock blockquote{margin:0;padding:0;border:0}
|
||||
.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
|
||||
.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
|
||||
.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right}
|
||||
.verseblock{margin:0 1em 1.25em}
|
||||
.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans-serif;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
|
||||
.verseblock pre strong{font-weight:400}
|
||||
.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
|
||||
.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
|
||||
.quoteblock .attribution br,.verseblock .attribution br{display:none}
|
||||
.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}
|
||||
.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none}
|
||||
.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0}
|
||||
.quoteblock.abstract{margin:0 1em 1.25em;display:block}
|
||||
.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center}
|
||||
.quoteblock.excerpt>blockquote,.quoteblock .quoteblock{padding:0 0 .25em 1em;border-left:.25em solid #dddddf}
|
||||
.quoteblock.excerpt,.quoteblock .quoteblock{margin-left:0}
|
||||
.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem}
|
||||
.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;font-size:.85rem;text-align:left;margin-right:0}
|
||||
p.tableblock:last-child{margin-bottom:0}
|
||||
td.tableblock>.content{margin-bottom:1.25em;word-wrap:anywhere}
|
||||
td.tableblock>.content>:last-child{margin-bottom:-1.25em}
|
||||
table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
|
||||
table.grid-all>*>tr>*{border-width:1px}
|
||||
table.grid-cols>*>tr>*{border-width:0 1px}
|
||||
table.grid-rows>*>tr>*{border-width:1px 0}
|
||||
table.frame-all{border-width:1px}
|
||||
table.frame-ends{border-width:1px 0}
|
||||
table.frame-sides{border-width:0 1px}
|
||||
table.frame-none>colgroup+*>:first-child>*,table.frame-sides>colgroup+*>:first-child>*{border-top-width:0}
|
||||
table.frame-none>:last-child>:last-child>*,table.frame-sides>:last-child>:last-child>*{border-bottom-width:0}
|
||||
table.frame-none>*>tr>:first-child,table.frame-ends>*>tr>:first-child{border-left-width:0}
|
||||
table.frame-none>*>tr>:last-child,table.frame-ends>*>tr>:last-child{border-right-width:0}
|
||||
table.stripes-all>*>tr,table.stripes-odd>*>tr:nth-of-type(odd),table.stripes-even>*>tr:nth-of-type(even),table.stripes-hover>*>tr:hover{background:#f8f8f7}
|
||||
th.halign-left,td.halign-left{text-align:left}
|
||||
th.halign-right,td.halign-right{text-align:right}
|
||||
th.halign-center,td.halign-center{text-align:center}
|
||||
th.valign-top,td.valign-top{vertical-align:top}
|
||||
th.valign-bottom,td.valign-bottom{vertical-align:bottom}
|
||||
th.valign-middle,td.valign-middle{vertical-align:middle}
|
||||
table thead th,table tfoot th{font-weight:bold}
|
||||
tbody tr th{background:#f7f8f7}
|
||||
tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
|
||||
p.tableblock>code:only-child{background:none;padding:0}
|
||||
p.tableblock{font-size:1em}
|
||||
ol{margin-left:1.75em}
|
||||
ul li ol{margin-left:1.5em}
|
||||
dl dd{margin-left:1.125em}
|
||||
dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
|
||||
li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
|
||||
ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none}
|
||||
ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em}
|
||||
ul.unstyled,ol.unstyled{margin-left:0}
|
||||
li>p:empty:only-child::before{content:"";display:inline-block}
|
||||
ul.checklist>li>p:first-child{margin-left:-1em}
|
||||
ul.checklist>li>p:first-child>.fa-square-o:first-child,ul.checklist>li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em}
|
||||
ul.checklist>li>p:first-child>input[type=checkbox]:first-child{margin-right:.25em}
|
||||
ul.inline{display:flex;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em}
|
||||
ul.inline>li{margin-left:1.25em}
|
||||
.unstyled dl dt{font-weight:400;font-style:normal}
|
||||
ol.arabic{list-style-type:decimal}
|
||||
ol.decimal{list-style-type:decimal-leading-zero}
|
||||
ol.loweralpha{list-style-type:lower-alpha}
|
||||
ol.upperalpha{list-style-type:upper-alpha}
|
||||
ol.lowerroman{list-style-type:lower-roman}
|
||||
ol.upperroman{list-style-type:upper-roman}
|
||||
ol.lowergreek{list-style-type:lower-greek}
|
||||
.hdlist>table,.colist>table{border:0;background:none}
|
||||
.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
|
||||
td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}
|
||||
td.hdlist1{font-weight:bold;padding-bottom:1.25em}
|
||||
td.hdlist2{word-wrap:anywhere}
|
||||
.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
|
||||
.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top}
|
||||
.colist td:not([class]):first-child img{max-width:none}
|
||||
.colist td:not([class]):last-child{padding:.25em 0}
|
||||
.thumb,.th{line-height:0;display:inline-block;border:4px solid #fff;box-shadow:0 0 0 1px #ddd}
|
||||
.imageblock.left{margin:.25em .625em 1.25em 0}
|
||||
.imageblock.right{margin:.25em 0 1.25em .625em}
|
||||
.imageblock>.title{margin-bottom:0}
|
||||
.imageblock.thumb,.imageblock.th{border-width:6px}
|
||||
.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
|
||||
.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
|
||||
.image.left{margin-right:.625em}
|
||||
.image.right{margin-left:.625em}
|
||||
a.image{text-decoration:none;display:inline-block}
|
||||
a.image object{pointer-events:none}
|
||||
sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}
|
||||
sup.footnote a,sup.footnoteref a{text-decoration:none}
|
||||
sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}
|
||||
#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
|
||||
#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0}
|
||||
#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em}
|
||||
#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em}
|
||||
#footnotes .footnote:last-of-type{margin-bottom:0}
|
||||
#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
|
||||
div.unbreakable{page-break-inside:avoid}
|
||||
.big{font-size:larger}
|
||||
.small{font-size:smaller}
|
||||
.underline{text-decoration:underline}
|
||||
.overline{text-decoration:overline}
|
||||
.line-through{text-decoration:line-through}
|
||||
.aqua{color:#00bfbf}
|
||||
.aqua-background{background:#00fafa}
|
||||
.black{color:#000}
|
||||
.black-background{background:#000}
|
||||
.blue{color:#0000bf}
|
||||
.blue-background{background:#0000fa}
|
||||
.fuchsia{color:#bf00bf}
|
||||
.fuchsia-background{background:#fa00fa}
|
||||
.gray{color:#606060}
|
||||
.gray-background{background:#7d7d7d}
|
||||
.green{color:#006000}
|
||||
.green-background{background:#007d00}
|
||||
.lime{color:#00bf00}
|
||||
.lime-background{background:#00fa00}
|
||||
.maroon{color:#600000}
|
||||
.maroon-background{background:#7d0000}
|
||||
.navy{color:#000060}
|
||||
.navy-background{background:#00007d}
|
||||
.olive{color:#606000}
|
||||
.olive-background{background:#7d7d00}
|
||||
.purple{color:#600060}
|
||||
.purple-background{background:#7d007d}
|
||||
.red{color:#bf0000}
|
||||
.red-background{background:#fa0000}
|
||||
.silver{color:#909090}
|
||||
.silver-background{background:#bcbcbc}
|
||||
.teal{color:#006060}
|
||||
.teal-background{background:#007d7d}
|
||||
.white{color:#bfbfbf}
|
||||
.white-background{background:#fafafa}
|
||||
.yellow{color:#bfbf00}
|
||||
.yellow-background{background:#fafa00}
|
||||
span.icon>.fa{cursor:default}
|
||||
a span.icon>.fa{cursor:inherit}
|
||||
.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
|
||||
.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c}
|
||||
.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
|
||||
.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900}
|
||||
.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400}
|
||||
.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000}
|
||||
.conum[data-value]{display:inline-block;color:#fff!important;background:rgba(0,0,0,.8);border-radius:50%;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
|
||||
.conum[data-value] *{color:#fff!important}
|
||||
.conum[data-value]+b{display:none}
|
||||
.conum[data-value]::after{content:attr(data-value)}
|
||||
pre .conum[data-value]{position:relative;top:-.125em}
|
||||
b.conum *{color:inherit!important}
|
||||
.conum:not([data-value]):empty{display:none}
|
||||
dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}
|
||||
h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em}
|
||||
p strong,td.content strong,div.footnote strong{letter-spacing:-.005em}
|
||||
p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem}
|
||||
p{margin-bottom:1.25rem}
|
||||
.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
|
||||
.exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc}
|
||||
.print-only{display:none!important}
|
||||
@page{margin:1.25cm .75cm}
|
||||
@media print{*{box-shadow:none!important;text-shadow:none!important}
|
||||
html{font-size:80%}
|
||||
a{color:inherit!important;text-decoration:underline!important}
|
||||
a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
|
||||
a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
|
||||
abbr[title]{border-bottom:1px dotted}
|
||||
abbr[title]::after{content:" (" attr(title) ")"}
|
||||
pre,blockquote,tr,img,object,svg{page-break-inside:avoid}
|
||||
thead{display:table-header-group}
|
||||
svg{max-width:100%}
|
||||
p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
|
||||
h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
|
||||
#header,#content,#footnotes,#footer{max-width:none}
|
||||
#toc,.sidebarblock,.exampleblock>.content{background:none!important}
|
||||
#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important}
|
||||
body.book #header{text-align:center}
|
||||
body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em}
|
||||
body.book #header .details{border:0!important;display:block;padding:0!important}
|
||||
body.book #header .details span:first-child{margin-left:0!important}
|
||||
body.book #header .details br{display:block}
|
||||
body.book #header .details br+span::before{content:none!important}
|
||||
body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
|
||||
body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
|
||||
.listingblock code[data-lang]::before{display:block}
|
||||
#footer{padding:0 .9375em}
|
||||
.hide-on-print{display:none!important}
|
||||
.print-only{display:block!important}
|
||||
.hide-for-print{display:none!important}
|
||||
.show-for-print{display:inherit!important}}
|
||||
@media amzn-kf8,print{#header>h1:first-child{margin-top:1.25rem}
|
||||
.sect1{padding:0!important}
|
||||
.sect1+.sect1{border:0}
|
||||
#footer{background:none}
|
||||
#footer-text{color:rgba(0,0,0,.6);font-size:.9em}}
|
||||
@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}}
|
||||
</style>
|
||||
</head>
|
||||
<body class="article">
|
||||
<div id="header">
|
||||
</div>
|
||||
<div id="content">
|
||||
<div class="sect1">
|
||||
<h2 id="_qqq_tables">QQQ Tables</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph">
|
||||
<p>The core type of object in a QQQ Instance is the Table.
|
||||
In the most common use-case, a QQQ Table may be the in-app representation of a Database table.
|
||||
That is, it is a collection of records (or rows) of data, each of which has a set of fields (or columns).</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<p>QQQ also allows other types of data sources (<a href="Backends{relfilesuffix}">QQQ Backends</a>) to be used as tables, such as File systems, API’s, Java enums or objects, etc.
|
||||
All of these backend types present the same interfaces (both user-interfaces, and application programming interfaces), regardless of their backend type.</p>
|
||||
</div>
|
||||
<div class="sect2">
|
||||
<h3 id="_qtablemetadata">QTableMetaData</h3>
|
||||
<div class="paragraph">
|
||||
<p>Tables are defined in a QQQ Instance in a <code><strong>QTableMetaData</strong></code> object.
|
||||
All tables must reference a <a href="Backends{relfilesuffix}">QQQ Backend</a>, a list of fields that define the shape of records in the table, and additional data to describe how to work with the table within its backend.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<p><strong>QTableMetaData Properties:</strong></p>
|
||||
</div>
|
||||
<div class="ulist">
|
||||
<ul>
|
||||
<li>
|
||||
<p><code>name</code> - <strong>String, Required</strong> - Unique name for the table within the QQQ Instance.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>label</code> - <strong>String</strong> - User-facing label for the table, presented in User Interfaces.
|
||||
Inferred from <code>name</code> if not set.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>backendName</code> - <strong>String, Required</strong> - Name of a <a href="Backends{relfilesuffix}">QQQ Backend</a> in which this table’s data is managed.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>fields</code> - <strong>Map of String → <a href="Fields{relfilesuffix}">QQQ Field</a>, Required</strong> - The columns of data that make up all records in this table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>primaryKeyField</code> - <strong>String, Conditional</strong> - Name of a <a href="Fields{relfilesuffix}">QQQ Field</a> that serves as the primary key (e.g., unique identifier) for records in this table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>uniqueKeys</code> - <strong>List of UniqueKey</strong> - Definition of additional unique constraints (from an RDBMS point of view) from the table.
|
||||
e.g., sets of columns which must have unique values for each record in the table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>backendDetails</code> - <strong>QTableBackendDetails or subclass</strong> - Additional data to configure the table within its <a href="Backends{relfilesuffix}">QQQ Backend</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>automationDetails</code> - <strong>QTableAutomationDetails</strong> - Configuration of automated jobs that run against records in the table, e.g., upon insert or update.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>customizers</code> - <strong>Map of String → QCodeReference</strong> - References to custom code that are injected into standard table actions, that allow applications to customize certain parts of how the table works.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>parentAppName</code> - <strong>String</strong> - Name of a <a href="Apps{relfilesuffix}">QQQ App</a> that this table exists within.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>icon</code> - <strong>QIcon</strong> - Icon associated with this table in certain user interfaces.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>recordLabelFormat</code> - <strong>String</strong> - Java Format String, used with <code>recordLabelFields</code> to produce a label shown for records from the table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>recordLabelFields</code> - <strong>List of String, Conditional</strong> - Used with <code>recordLabelFormat</code> to provide values for any format specifiers in the format string.
|
||||
These strings must be field names within the table.</p>
|
||||
<div class="ulist">
|
||||
<ul>
|
||||
<li>
|
||||
<p>Example of using <code>recordLabelFormat</code> and <code>recordLabelFields</code>:</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="listingblock">
|
||||
<div class="content">
|
||||
<pre class="CodeRay highlight"><code data-lang="java"><span style="color:#777">// given these fields in the table:</span>
|
||||
<span style="color:#080;font-weight:bold">new</span> QFieldMetaData(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">name</span><span style="color:#710">"</span></span>, QFieldType.STRING)
|
||||
<span style="color:#080;font-weight:bold">new</span> QFieldMetaData(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">birthDate</span><span style="color:#710">"</span></span>, QFieldType.DATE)
|
||||
|
||||
<span style="color:#777">// We can produce a record label such as "Darin Kelkhoff (1980-05-31)" via:</span>
|
||||
.withRecordLabelFormat(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">%s (%s)</span><span style="color:#710">"</span></span>)
|
||||
.withRecordLabelFields(<span style="color:#0a8;font-weight:bold">List</span>.of(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">name</span><span style="color:#710">"</span></span>, <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">birthDate</span><span style="color:#710">"</span></span>))</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ulist">
|
||||
<ul>
|
||||
<li>
|
||||
<p><code>sections</code> - <strong>List of QFieldSection</strong> - Mechanism to organize fields within user interfaces, into logical sections.
|
||||
If any sections are present in the table meta data, then all fields in the table must be listed in exactly 1 section.
|
||||
If no sections are defined, then instance enrichment will define default sections.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>associatedScripts</code> - <strong>List of AssociatedScript</strong> - Definition of user-defined scripts that can be associated with records within the table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>enabledCapabilities</code> and <code>disabledCapabilities</code> - <strong>Set of Capability enum values</strong> - Overrides from the backend level, for capabilities that this table does or does not possess.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<div id="footer-text">
|
||||
Last updated 2022-11-21 09:02:56 -0600
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -2,16 +2,50 @@
|
||||
== Widgets
|
||||
include::../variables.adoc[]
|
||||
|
||||
#TODO#
|
||||
Widgets are the most customizable UI components in QQQ.
|
||||
They can be used either on App Home Screens (e.g., as Dashboard screens),
|
||||
or they can be included into Record View screens.
|
||||
|
||||
QQQ defines several types of widgets, such as charts (pie, bar, line),
|
||||
numeric displays, application-populated tables, or even fully custom HTML.
|
||||
|
||||
=== QWidgetMetaData
|
||||
A Widget is defined in a QQQ Instance in a `*QWidgetMetaData*` object.
|
||||
|
||||
#TODO#
|
||||
|
||||
*QWidgetMetaData Properties:*
|
||||
|
||||
* `name` - *String, Required* - Unique name for the widget within the QQQ Instance.
|
||||
* `type` - *String, Required* - Specifies the UI & data type for the widget.
|
||||
* `label` - *String* - User-facing header or title for a widget.
|
||||
* `tooltip` - *String* - Text contents to be placed in a tooltip associated with the widget's label in the UI.
|
||||
** Values should come from the `WidgetType` enum's `getType()` method (e.g., `WidgetType.BAR_CHART.getType()`)
|
||||
* `gridColumns` - *Integer* - for a desktop-sized screen, in a 12-based grid,
|
||||
how many columns the widget should consume.
|
||||
* `codeReference` - *QCodeReference, Required* - Reference to the custom code,
|
||||
a subclass of `AbstractWidgetRenderer`, which is responsible for loading data to render the widget.
|
||||
* `footerHTML` - *String* - HTML String, which, if present, will be displayed in the
|
||||
footer of the widget (not supported by all widget types).
|
||||
* `isCard` - *boolean, default false* #TODO#
|
||||
* `showReloadButton` - *boolean, default true* #TODO#
|
||||
* `showExportButton` - *boolean, default false* #TODO#
|
||||
* `dropdowns` - #TODO#
|
||||
* `storeDropdownSelections` - *boolean* #TODO#
|
||||
* `icons` - *Map<String, QIcon>* #TODO#
|
||||
* `defaultValues` - *Map<String, Serializable>* #TODO#
|
||||
|
||||
#TODO#
|
||||
There are also some subclasses of `QWidgetMetaData`, for some specific widget types:
|
||||
|
||||
*ParentWidgetMetaData Properties:*
|
||||
|
||||
* `title` - *String* #TODO - how does this differ from label?#
|
||||
* `childWidgetNameList` - *List<String>, Required*
|
||||
* `childProcessNameList` - *List<String>* #TODO appears unused - check, and delete#
|
||||
* `laytoutType` - *enum of GRID or TABS, default GRID*
|
||||
|
||||
*QNoCodeWidgetMetaData Properties:*
|
||||
|
||||
* `values` - *List<AbstractWidgetValueSource>* #TODO#
|
||||
* `outputs` - *List<AbstractWidgetOutput>* #TODO#
|
||||
|
||||
#TODO - Examples#
|
||||
|
||||
|
155
docs/misc/ProcessBackendSteps.adoc
Normal file
155
docs/misc/ProcessBackendSteps.adoc
Normal file
@ -0,0 +1,155 @@
|
||||
== Process Backend Steps
|
||||
include::../variables.adoc[]
|
||||
|
||||
In many QQQ applications, much of the code that engineers write will take the form of Backend Steps for {link-processes}.
|
||||
Such code is defined in classes which implement the interface `BackendStep`.
|
||||
This interface defines only a single method:
|
||||
[source,java]
|
||||
.BackendStep.java
|
||||
----
|
||||
public interface BackendStep
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Execute the backend step - using the request as input, and the result as output.
|
||||
**
|
||||
*******************************************************************************/
|
||||
void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException;
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
Process backend steps have access to state information - specifically, a list of records, and a map of name=value pairs - in the input & output objects.
|
||||
This state data is persisted by QQQ between steps (e.g., if a frontend step is presented to a user between backend steps).
|
||||
|
||||
|
||||
=== RunBackendStepInput
|
||||
All input data to the step is available in the `RunBackendStepInput` object.
|
||||
Key methods in this class are:
|
||||
|
||||
* `getRecords()` - Returns the List of <<QRecords>> that are currently being acted on in the process.
|
||||
* `getValues()` - Returns a Map of String -> Serializable; name=value pairs that are the state-data of the process.
|
||||
** Values can be added to this state from a process's meta-data, from a screen, or from another backend step.
|
||||
* `getValue(String fieldName)` - Returns a specific value, by name, from the process's state.
|
||||
** This method has several variations that return the value as a specific type, such as `getValueString`, `getValueInteger`, `getValueBoolean`...
|
||||
* `getAsyncJobCallback()` - Accessor for an `AsyncJobCallback` object, which provides a way for the process backend step to communicate about its status or progress with a user running the process in a frontend.
|
||||
Provides methods:
|
||||
** `updateStatus(String message)` - Where general status messages can be given.
|
||||
For example, `"Loading census data"`
|
||||
** `updateStatus(int current, int total)` - For updating a progress meter.
|
||||
e.g., "47 of 1701" would be display by calling `.updateStatus(47, 1701)`
|
||||
* `getFrontendStepBehavior()` - Enum, indicating what should happen when a frontend step is encountered as the process's next step to run.
|
||||
Possible values are:
|
||||
** `BREAK` - Indicates that the process's execution should be suspended, so that the screen represented by the frontend step can be presented to a user.
|
||||
This would be the expected behavior if a process is being run by a user from a UI.
|
||||
** `SKIP` - Indicates that frontend steps should be skipped.
|
||||
This would be the expected behavior if a process is running from a scheduled job (without a user present to drive it), for example.
|
||||
** `FAIL` - Indicates that the process should end with an exception if a frontend step is encountered.
|
||||
** A backend step may want to act differently based on its frontendStepBehavior.
|
||||
For example, additional data may be looked up for displaying to a user if the behavior is `BREAK`.
|
||||
* `getBasepullLastRunTime()` - For <<BasepullConfiguration,Basepull>> processes, this is the `Instant` stored in the basepull table as the process's last run time.
|
||||
|
||||
=== RunBackendStepOutput
|
||||
All output from a process step should be placed in its `RunBackendStepOutput` object (and/or stored to a backend, as appropriate).
|
||||
Key methods in this class are:
|
||||
|
||||
* `addValue(String fieldName, Serializable value)` - Adds a single named value to the process's state, overwriting it the value if it already exists.
|
||||
* `addRecord(QRecord record)` - Add a `<<QRecord>>` to the process's output.
|
||||
* `addAuditSingleInput(AuditSingleInput auditSingleInput)` - Add a new entry to the process's list of audit inputs, to be stored at the completion of the process.
|
||||
** An `AuditSingleInput` object can most easily be built with the constructor: `AuditSingleInput(String tableName, QRecord record, String auditMessage)`.
|
||||
** Additional audit details messages (sub-bullets that accompany the primary `auditMessage`) can be added to an `AuditSingleInput` via the `addDetail(String message)` method.
|
||||
** _Note that at this time, the automatic storing of these audits is only provided by the execute step of a StreamedETLWithFrontendProcesses._
|
||||
|
||||
=== Example
|
||||
|
||||
[source,java]
|
||||
.Example of a BackendStep
|
||||
----
|
||||
/*******************************************************************************
|
||||
** For the "person" table's "Add Age" process -
|
||||
** For each input person record, add the specified yearsToAdd to their age.
|
||||
*******************************************************************************/
|
||||
public class AddAge implements BackendStep
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// get the yearsToAdd input field value from the process input //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
Integer yearsToAdd = runBackendStepInput.getValueInteger("yearsToAdd");
|
||||
int totalYearsAdded = 0;
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// loop over the records passed into the process //
|
||||
///////////////////////////////////////////////////
|
||||
for(QRecord record : runBackendStepInput.getRecords())
|
||||
{
|
||||
Integer age = record.getValueInteger("age");
|
||||
age += yearsToAdd;
|
||||
totalYearsAdded += yearsToAdd;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// update the record with the new "age" value. //
|
||||
// note that this update record object will implicitly be available to the process's next //
|
||||
// backend step, via the sharing of the processState object. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
record.setValue("age", age);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
// set an output value for the process //
|
||||
/////////////////////////////////////////
|
||||
runBackendStepOutput.addValue("totalYearsAdded", totalYearsAdded);
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
|
||||
=== Backend Steps for StreamedETLWithFrontendProcesses
|
||||
|
||||
For <<StreamedETLWithFrontendProcess>> type processes, backend steps are defined a little bit differently than they are for other process types.
|
||||
In this type of process, the process meta-data defines 3 backend steps which are built-in to QQQ, and which do not have any custom application logic.
|
||||
These steps are:
|
||||
|
||||
* `StreamedETLPreviewStep`
|
||||
* `StreamedETLValidateStep`
|
||||
* `StreamedETLExecuteStep`
|
||||
|
||||
For custom application logic to be implemented in a StreamedETLWithFrontendProcesses, an application engineer must define (up to) 3 backend step classes which are loaded by the steps listed above.
|
||||
These application-defined steps must extend specific classes (which themselves implement the `BackendStep` interface), to provide the needed logic of this style of process.
|
||||
These steps are:
|
||||
|
||||
* *Extract* - a subclass of `AbstractExtractStep` - is responsible for Extracting records from the source table.
|
||||
** For this step, we can often use the QQQ-provided class `ExtractViaQueryStep`, or sometimes a subclass of it.
|
||||
** The Extract step is called before the Preview, Validate, and Result screens, though for the Preview screen, it is set to only extract a small number of records (10).
|
||||
* *Transform* - a subclass of `AbstractTransformStep` - is responsible for applying the majority of the business logic of the process.
|
||||
In ETL terminology, this is the "Transform" action - which means applying some type of logical transformation an input record (found by the Extract step) to generate an output record (stored by the Load step).
|
||||
** A Transform step's `run` method will be called, potentially, multiple times, each time with a page of records in the `runBackendStepInput` parameter.
|
||||
** This method is responsible for adding records to the `runBackendStepOutput`, which will then be passed to the *Load* step.
|
||||
** This class is also responsible for implementing the method `getProcessSummary`, which provides the data to the *Validate* screen.
|
||||
** The run method will generally update ProcessSummaryLine objects to facilitate this functionality.
|
||||
** The Transform step is called before the Preview, Validate, and Result screens, consuming all records selected by the Extract step.
|
||||
* *Load* - a subclass of `AbstractLoadStep` - is responsible for the Load function of the ETL job.
|
||||
_A quick word on terminology - this step is actually doing what we are more likely to think of as storing data - which feels like the opposite of “loading” - but we use the name Load to keep in line with the ETL naming convention…_
|
||||
** The Load step is ONLY called before the Result screen is presented (possibly after Preview, if the user chose to skip validation, otherwise, after validation).
|
||||
** Similar to the Transform step, the Load step's `run` method will be called potentially multiple times, with pages of records in its input.
|
||||
** As such, the Load step is generally the only step where data writes should occur.
|
||||
*** e.g., a Transform step should not do any writes, as it will be called when the user is going to the Preview & Validate screens - e.g., before the user confirmed that they want to execute the action!
|
||||
** A common pattern is that the Load step just needs to insert or update the list of records output by the Transform step, in which case the QQQ-provided `LoadViaInsertStep` or `LoadViaUpdateStep` can be used, but custom use-cases can be built as well.
|
||||
|
||||
Another distinction between StreamedELTWithFrontendProcess steps and general QQQ process backend steps, is that the list of records in the input & output objects is NOT shared for StreamedELTWithFrontendProcess steps.
|
||||
The direct implication of this is, that a Transform step MUST explicitly call `output.addRecord()` for any records that it wants to pass along to the Load step.
|
||||
|
||||
==== Example
|
||||
|
||||
[source,java]
|
||||
.Examples of a Transform and Load step for a StreamedELTWithFrontendProcess
|
||||
----
|
||||
// todo!
|
||||
----
|
||||
|
||||
#todo: more details on these 3 specialized types of process steps (e.g., method to overload, when stuff like pre-action is called; how summaries work).#
|
30
docs/misc/QContext.adoc
Normal file
30
docs/misc/QContext.adoc
Normal file
@ -0,0 +1,30 @@
|
||||
== QContext
|
||||
include::../variables.adoc[]
|
||||
|
||||
The class `QContext` contains a collection of thread-local variables, to define the current context of the QQQ code that is currently running.
|
||||
For example, what `QInstance` (meta-data container) is being used, what `QSession` (user attributes) is active, etc.
|
||||
|
||||
Most of the time, main-line application code does not need to worry about setting up the `QContext` - although unit-test code can is a common exception to that rule.
|
||||
This is because all of QQQ's entry-points into execution (e.g., web context handlers, CLI startup methods, schedulers, and multi-threaded executors) take care of initializing the context.
|
||||
|
||||
It is more common though, for application code to need to get data from the context, such as the current session or any piece of meta-data from the QInstance.
|
||||
The methods to access data from the `QContext` are fairly straightforward:
|
||||
|
||||
=== Examples
|
||||
==== Get a QTableMetaData from the active QInstance
|
||||
[source,java]
|
||||
----
|
||||
QTableMeataData myTable = QContext.getQInstance().getTable("myTable");
|
||||
for(QFieldMeataData field : myTable.getFields().values())
|
||||
{
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
==== Get a security key value for the current user session
|
||||
[source,java]
|
||||
----
|
||||
QSession session = QContext.getQSession();
|
||||
List<Serializable> clientIds = session.getSecurityKeyValues("clientId");
|
||||
----
|
||||
|
116
docs/misc/QRecordEntities.adoc
Normal file
116
docs/misc/QRecordEntities.adoc
Normal file
@ -0,0 +1,116 @@
|
||||
== QRecordEntities
|
||||
include::../variables.adoc[]
|
||||
|
||||
While `<<QRecords>>` provide a flexible mechanism for passing around record data in a QQQ Application, they have one big disadvantage from the point-of-view of a Java Application:
|
||||
They do not provide a mechanism to ensure compile-time checks of field names or field types.
|
||||
|
||||
As such, an alternative mechanism exists, which allows records in a QQQ application to be worked with following a more familiar Java Bean (Entity Bean) like pattern.
|
||||
This mechanism is known as a `QRecordEntity`.
|
||||
|
||||
Specifically speaking, `QRecordEntity` is an abstract base class, whose purpose is to be the base class for entity-bean classes.
|
||||
Using reflection, `QRecordEntity` is able to convert objects back and forth from `QRecord` to specific entity-bean subtypes.
|
||||
|
||||
For example, the method `QRecordEntity::toQRecord()` converts a record entity object into a `QRecord`.
|
||||
|
||||
Inversely, `QRecordEntity::populateFromQRecord(QRecord record)` sets fields in a record entity object, based on the values in the supplied `QRecord`.
|
||||
It is conventional for a subclass of `QRecordEntity` to have both a no-arg (default) constructor, and a constructor that takes a `QRecord` as a parameter, and calls `populateFromQRecord`.
|
||||
|
||||
In addition to these constructors, a `QRecordEntity` subclass will generally contain:
|
||||
|
||||
* A `public static final String TABLE_NAME`, used throughout the application as a constant reference to the name for the {link-table}.
|
||||
* A series of `private` fields, corresponding to the fields in the table that the entity represents.
|
||||
** If these fields are annotated as `@QField()`, then the {link-table} meta-data for the table that the entity represents can in part be inferred by QQQ, by using the method `QTableMetaData::withFieldsFromEntity`.
|
||||
* `getX()`, `setX()`, and `withX()` methods for all of the entity's fields.
|
||||
|
||||
=== Examples
|
||||
[source,java]
|
||||
.Example Definition of a QRecordEntity subclass: Person.java
|
||||
----
|
||||
/*******************************************************************************
|
||||
** QRecordEntity for the person table.
|
||||
*******************************************************************************/
|
||||
public class Person extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "person";
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Integer id;
|
||||
|
||||
@QField()
|
||||
private String firstName;
|
||||
|
||||
@QField()
|
||||
private String lastName;
|
||||
|
||||
@QField()
|
||||
private Integer age;
|
||||
|
||||
// all other fields
|
||||
|
||||
/*******************************************************************************
|
||||
** Default constructor
|
||||
*******************************************************************************/
|
||||
public Person()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor that takes a QRecord
|
||||
*******************************************************************************/
|
||||
public Person(QRecord record)
|
||||
{
|
||||
populateFromQRecord(record);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Custom method to concatenate firstName and lastName
|
||||
*******************************************************************************/
|
||||
public String getFullName()
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// if there were more than an example, we could do some null-checks //
|
||||
// here to avoid silly output like "Bobby null" :) //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
return (firstName + " " + lastName);
|
||||
}
|
||||
|
||||
// all getter, setter, and fluent setter (wither) methods
|
||||
}
|
||||
----
|
||||
|
||||
The core ORM actions and process-execution actions of QQQ work with `QRecords`.
|
||||
However, application engineers may want to apply patterns like the following example, to gain the compile-time safety of `QRecordEntities`:
|
||||
|
||||
[source,java]
|
||||
.Example Usage of a QRecordEntity
|
||||
----
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// assume we're working with the "person" table & entity from above //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> recordsToUpdate = new ArrayList<>();
|
||||
for(QRecord record : inputRecordList)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// call the constructor that copies values from the record into the entity //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
Person person = new Person(record);
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// call a custom method defined in the entity //
|
||||
////////////////////////////////////////////////
|
||||
LOG.info("Processing: " + person.getFullName());
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// age is an Integer, so no type-conversion is needed here //
|
||||
/////////////////////////////////////////////////////////////
|
||||
person.setAge(person.getAge() + 1);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// to pass the updated records to UpdateAction, convert them to QRecords //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
recordsToUpdate.add(person.toQRecord());
|
||||
}
|
||||
----
|
||||
|
68
docs/misc/QRecords.adoc
Normal file
68
docs/misc/QRecords.adoc
Normal file
@ -0,0 +1,68 @@
|
||||
== QRecords
|
||||
include::../variables.adoc[]
|
||||
|
||||
Almost all code inside a QQQ application will be dealing with *Records* (aka Tuples or Rows).
|
||||
That is: a collection of named values, representing a single Entity, Fact, or, Row from a {link-table}.
|
||||
|
||||
The class that QQQ uses to work with records is called: `QRecord`.
|
||||
|
||||
=== Values
|
||||
At its core, a `QRecord` is a wrapper around a `Map<String, Serializable> values`.
|
||||
These are the *actual* values for the fields in the table for the record.
|
||||
That is, direct representations of the values as they are stored in the {link-backend}.
|
||||
|
||||
The keys in the `values` map are names from the {link-fields} in the {link-table}.
|
||||
|
||||
The values in `values` map are declared as `Serializable` (to help ensure the serializability of the `QRecord` as a whole).
|
||||
In practice, their types will be based on the `QFieldType` of the {link-field} that they correspond to.
|
||||
That will typically be one of: `String`, `Integer`, `Boolean`, `BigDecimal`, `Instant`, `LocalDate`, `LocalTime`, or `byte[]`.
|
||||
Be aware that `null` values may be in the `values` map, especially/per if the backend/table support `null`.
|
||||
|
||||
To work with the `values` map, the following methods are provided:
|
||||
|
||||
* `setValue(String fieldName, Serializable value)` - Sets a value for the specified field in the record.
|
||||
** Overloaded as `setValue(String fieldName, Object value)` - For some cases where the value may not be known to be `Serializable`.
|
||||
In this overload, if the value is `null` or `Serializable`, the primary version of `setValue` is called.
|
||||
Otherwise, the `value` is passed through `String::valueOf`, and the result is stored.
|
||||
** Overloaded as `setValue(QFieldMetaData field, Serializable value)` - which simply defers to the primary version of `setValue`, passing `field.getName()` as the first parameter.
|
||||
* `removeValue(String fieldName)` - Remove the given field from the `values` map.
|
||||
** Note that in some situations this is an important distinction from having a `null` value in the map (See <<UpdateAction)>>).
|
||||
* `setValues(Map<String, Serializable> values)` - Sets the full map of `values`.
|
||||
* `getValues()` - Returns the full map of `values`.
|
||||
* `getValue(String fieldName)` - Returns the value for the named field - possibly `null` - as a `Serializable`.
|
||||
* Several type-specific variations of `getValueX(String fieldName)`, where internally, values will be not exactly type-cast, but effectively converted (if possible) to the requested type.
|
||||
These conversions are done using the `ValueUtils.getValueAsX(Object)` methods.
|
||||
These methods are generally the preferred/cleanest way to get record values in application code, when it is needed in a type-specific way .
|
||||
** `getValueString(String fieldName)`
|
||||
** `getValueInteger(String fieldName)`
|
||||
** `getValueBoolean(String fieldName)`
|
||||
** `getValueBigDecimal(String fieldName)`
|
||||
** `getValueInstant(String fieldName)`
|
||||
** `getValueLocalDate(String fieldName)`
|
||||
** `getValueLocalTime(String fieldName)`
|
||||
** `getValueByteArray(String fieldName)`
|
||||
|
||||
=== Display Values
|
||||
In addition to the `values` map, a `QRecord` contains another map called `displayValues`, which only stores `String` values.
|
||||
That is to say, values for other types are stringified, based on their {link-field}'s type and `displayFormat` property.
|
||||
In addition, fields which have a `possibleValueSource` property will have their translated values set in the `displayValues` map.
|
||||
|
||||
By default, a `QRecord` will not have its `displayValues` populated.
|
||||
To populate `displayValues`, the <<QueryAction>> and <<GetAction>> classes take a property in their inputs called `shouldGenerateDisplayValues`, which must be set to `true` to generate `displayValues`.
|
||||
In addition, these two actions also have a property `shouldTranslatePossibleValues` in their inputs, which needs to be set to `true` if possible-value lookups are to be performed.
|
||||
|
||||
As an alternative to the `shouldGenerateDisplayValues` and `shouldTranslatePossibleValues` inputs to <<QueryAction>> and <<GetAction>>, one can directly call the `QValueFormatter.setDisplayValuesInRecords` and/or `qPossibleValueTranslator.translatePossibleValuesInRecords` methods.
|
||||
Or, for special cases, `setDisplayValue(String fieldName, String displayValue)` or `setDisplayValues(Map<String, String> displayValues)` can be called directly.
|
||||
|
||||
=== Backend Details
|
||||
Sometimes a backend may want to place additional data in a `QRecord` that doesn't exactly correspond to a field.
|
||||
To do this, the `Map<String, Serializable> backendDetails` member is used.
|
||||
|
||||
For example, an API backend may store the full JSON `String` that came from the API as a backend detail in a `QRecord`.
|
||||
Or fields that are marked as `isHeavy`, if the full (heavy) value of the field hasn't been fetched, then the lengths of any such heavy fields may be stored in `backendDetails`.
|
||||
|
||||
=== Errors and Warnings
|
||||
#todo#
|
||||
|
||||
=== Associated Records
|
||||
#todo#
|
224
docs/misc/RenderingWidgets.adoc
Normal file
224
docs/misc/RenderingWidgets.adoc
Normal file
@ -0,0 +1,224 @@
|
||||
== Rendering Widgets
|
||||
include::../variables.adoc[]
|
||||
|
||||
=== WidgetRenderer classes
|
||||
In general, to fully implement a Widget, you must define its `QWidgetMetaData`,
|
||||
and supply a subclass of `AbstractWidgetRenderer`, to provide the data to the widget.
|
||||
(Note the "No Code" category of widgets, which are an exception to this generalization).
|
||||
|
||||
The only method required in a subclass of `AbstractWidgetRenderer` is:
|
||||
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
|
||||
The fields available in `RenderWidgetInput` are:
|
||||
|
||||
- `Map<String, String> queryParams` - These are parameters supplied by the frontend, for example,
|
||||
if a user selected values from dropdowns to control a dimension of your widget, those name/value
|
||||
pairs would be in this map. Similarly, if your widget is being included on a record view screen, then
|
||||
the record's primary key will be in this map.
|
||||
- `QWidgetMetaDataInterface widgetMetaData` - This is the meta-data for the widget being
|
||||
rendered. This can be useful in case you are using the same renderer class for multiple widgets.
|
||||
|
||||
The only field in `RenderWidgetOutput` is:
|
||||
|
||||
- `QWidgetData widgetData` - This is a base class, with several attributes, and more importantly,
|
||||
several subclasses, specific to the type of widget that is being rendered.
|
||||
|
||||
==== Widget-Type Specific Rendering Details
|
||||
Different widget types expect & require different types of values to be set in the `RenderWidgetOutput` by their renderers.
|
||||
|
||||
===== Pie Chart
|
||||
The `WidgetType.PIE_CHART` requires an object of type `ChartData`.
|
||||
The fields on this type are:
|
||||
|
||||
* `chartData` an instance of `ChartData.Data`, which has the following fields:
|
||||
** `labels` - *List<String>, required* - the labels for the slices of the pie.
|
||||
** `datasets` - *List<Dataset> required* - the data for each slice of the pie.
|
||||
For a Pie chart, only a single entry in this list is used
|
||||
(other chart types using `ChartData` may support more than 1 entry in this list).
|
||||
Fields in this object are:
|
||||
*** `label` - *String, required* - a label to describe the dataset as a whole.
|
||||
e.g., "Orders" for a pie showing orders of different statuses.
|
||||
*** `data` - *List<Number>, required* - the data points for each slice of the pie.
|
||||
*** `color` - *String* - HTML color for the slice
|
||||
*** `urls` - *List<String>* - Optional URLs for slices of the pie to link to when clicked.
|
||||
*** `backgroundColors` - *List<String>* - Optional HTML color codes for each slice of the pie.
|
||||
|
||||
[source,java]
|
||||
.Pie chart widget example
|
||||
----
|
||||
// meta data
|
||||
new QWidgetMetaData()
|
||||
.withName("pieChartExample")
|
||||
.withType(WidgetType.PIE_CHART.getType())
|
||||
.withGridColumns(4)
|
||||
.withIsCard(true)
|
||||
.withLabel("Pie Chart Example")
|
||||
.withCodeReference(new QCodeReference(PieChartExampleRenderer.class));
|
||||
|
||||
// renderer
|
||||
private List<String> labels = new ArrayList<>();
|
||||
private List<String> colors = new ArrayList<>();
|
||||
private List<Number> data = new ArrayList<>();
|
||||
|
||||
/*******************************************************************************
|
||||
** helper method - to add values for a slice to the lists
|
||||
*******************************************************************************/
|
||||
private void addSlice(String label, String color, Number datum)
|
||||
{
|
||||
labels.add(label);
|
||||
colors.add(color);
|
||||
data.add(datum);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** main method of the widget renderer
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
addSlice("Apple", "#FF0000", 100);
|
||||
addSlice("Orange", "#FF8000", 150);
|
||||
addSlice("Banana", "#FFFF00", 75);
|
||||
addSlice("Lime", "#00FF00", 100);
|
||||
addSlice("Blueberry", "#0000FF", 200);
|
||||
|
||||
ChartData chartData = new ChartData()
|
||||
.withChartData(new ChartData.Data()
|
||||
.withLabels(labels)
|
||||
.withDatasets(List.of(
|
||||
new ChartData.Data.Dataset()
|
||||
.withLabel("Flavor")
|
||||
.withData(data)
|
||||
.withBackgroundColors(colors)
|
||||
.withUrls(urls))));
|
||||
|
||||
return (new RenderWidgetOutput(chartData));
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
===== Bar Chart
|
||||
#todo#
|
||||
|
||||
===== Stacked Bar Chart
|
||||
#todo#
|
||||
|
||||
===== Horizontal Bar Chart
|
||||
#todo#
|
||||
|
||||
===== Child Record List
|
||||
#todo#
|
||||
|
||||
===== Line Chart
|
||||
#todo#
|
||||
|
||||
===== Small Line Chart
|
||||
#todo#
|
||||
|
||||
===== Statistics
|
||||
#todo#
|
||||
|
||||
===== Parent Widget
|
||||
#todo#
|
||||
|
||||
===== Composite
|
||||
A `WidgetType.COMPOSITE` is built by using one or more smaller elements, known as `Blocks`.
|
||||
Note that `Blocks` can also be used in the data of some other widget types
|
||||
(specifically, within a cell of a Table-type widget, or (in the future?) as a header above a pie or bar chart).
|
||||
|
||||
A composite widget renderer must return data of type `CompositeWidgetData`,
|
||||
which has the following fields:
|
||||
|
||||
* `blocks` - *List<AbstractBlockWidgetData>, required* - The blocks (1 or more) being composited together to make the widget.
|
||||
See below for details on the specific Block types.
|
||||
* `styleOverrides` - *Map<String, Serializable>* - Optional map of CSS attributes
|
||||
(named following javascriptStyleCamelCase) to apply to the `<div>` element that wraps the rendered blocks.
|
||||
* `layout` - *Layout enum* - Optional specifier for how the blocks should be laid out.
|
||||
e.g., predefined sets of CSS attributes to achieve specific layouts.
|
||||
** Note that some blocks are designed to work within composites with specific layouts.
|
||||
Look for matching names, such as `Layout.BADGES_WRAPPER` to go with `NumberIconBadgeBlock`.
|
||||
|
||||
[source,java]
|
||||
.Composite widget example - consisting of 3 Progress Bar Blocks, and one Divider Block
|
||||
----
|
||||
// meta data
|
||||
new QWidgetMetaData()
|
||||
.withName("compositeExample")
|
||||
.withType(WidgetType.COMPOSITE.getType())
|
||||
.withGridColumns(4)
|
||||
.withIsCard(true)
|
||||
.withLabel("Composite Example")
|
||||
.withCodeReference(new QCodeReference(CompositeExampleRenderer.class));
|
||||
|
||||
// renderer
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
CompositeWidgetData data = new CompositeWidgetData();
|
||||
|
||||
data.addBlock(new ProgressBarBlockData()
|
||||
.withValues(new ProgressBarValues()
|
||||
.withHeading("Blocks")
|
||||
.withPercent(new BigDecimal("78.5"))));
|
||||
|
||||
data.addBlock(new ProgressBarBlockData()
|
||||
.withValues(new ProgressBarValues()
|
||||
.withHeading("Progress")
|
||||
.withPercent(new BigDecimal(0))));
|
||||
|
||||
data.addBlock(new DividerBlockData());
|
||||
|
||||
data.addBlock(new ProgressBarBlockData()
|
||||
.withStyles(new ProgressBarStyles().withBarColor("#C0C000"))
|
||||
.withValues(new ProgressBarValues()
|
||||
.withHeading("Custom Color")
|
||||
.withPercent(new BigDecimal("75.3"))));
|
||||
|
||||
return (new RenderWidgetOutput(data));
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
===== Table
|
||||
#todo#
|
||||
|
||||
===== HTML
|
||||
#todo#
|
||||
|
||||
===== Divider
|
||||
#todo#
|
||||
|
||||
===== Process
|
||||
#todo#
|
||||
|
||||
===== Stepper
|
||||
#todo#
|
||||
|
||||
===== Data Bag Viewer
|
||||
#todo#
|
||||
|
||||
===== Script Viewer
|
||||
#todo#
|
||||
|
||||
=== Block-type Specific Rendering Details
|
||||
For Composite-type widgets (or other widgets which can include blocks),
|
||||
there are specific data classes required to be returned by the widget renderer.
|
||||
|
||||
Each block type defines a subclass of `AbstractBlockWidgetData`,
|
||||
which is a generic class with 3 type parameters:
|
||||
|
||||
* `V` - an implementation of `BlockValuesInterface` - to define the type of values that the block uses.
|
||||
* `S` - an implementation of `BlockSlotsInterface` (expected to be an `enum`) - to define the "slots" in the block,
|
||||
that can have Tooltips and/or Links applied to them.
|
||||
* `SX` - an implementation of `BlockStylesInterface` - to define the types of style customizations that the block supports.
|
||||
|
||||
These type parameters are designed to ensure type-safety for the application developer,
|
||||
to ensure that only
|
||||
|
||||
=== Additional Tips
|
||||
|
||||
* To make a Dashboard page (e.g., an App consisting of Widgets) with a parent widget use the parent widget's label as the page's label:
|
||||
** On the `QAppMetaData` that contains the Parent widget, call
|
||||
`.withSupplementalMetaData(new MaterialDashboardAppMetaData().withShowAppLabelOnHomeScreen(false))`.
|
||||
** In the Parent widget's renderer, on the `ParentWidgetData`, call `setLabel("My Label")` and
|
||||
`setIsLabelPageTitle(true)`.
|
141
docs/misc/ScheduledJobs.adoc
Normal file
141
docs/misc/ScheduledJobs.adoc
Normal file
@ -0,0 +1,141 @@
|
||||
== Schedulers and Scheduled Jobs
|
||||
include::../variables.adoc[]
|
||||
|
||||
QQQ has the ability to automatically run various types of jobs on schedules,
|
||||
either defined in your instance's meta-data,
|
||||
or optionally via data in your application, in a `scheduledJob` table.
|
||||
|
||||
=== Schedulers and QSchedulerMetaData
|
||||
2 types of schedulers are included in QQQ by default (though an application can define its own schedulers):
|
||||
|
||||
* `SimpleScheduler` - is (as its name suggests) a simple class which uses java's `ScheduledExecutorService`
|
||||
to run jobs on repeating intervals.
|
||||
** Cannot run cron schedules - only repeating intervals.
|
||||
** If multiple servers are running, each will potentially run the same job concurrently
|
||||
** Has no configurations, e.g., to limit the number of threads.
|
||||
|
||||
* `QuartzScheduler` - uses the 3rd party https://www.quartz-scheduler.org/[Quartz Scheduler] library to provide
|
||||
a much more capable, though admittedly more complex, scheduling solution.
|
||||
** Can run both cron schedules and repeating intervals.
|
||||
** By default, will not allow concurrent executions of the same job.
|
||||
** Supports multiple configurations, e.g., to limit the number of threads.
|
||||
|
||||
An application can define its own scheduler by providing a class which implements the `QSchedulerInterface`.
|
||||
|
||||
A `QInstance` can work with 0 or more schedulers, as defined by adding `QSchedulerMetaData` objects
|
||||
to the instance.
|
||||
|
||||
This meta-data class is `abstract`, and is extended by the 2 built-in schedulers
|
||||
(e.g., `SimpleSchedulerMetaData` and `QuartzSchedulerMetaData`). As such,
|
||||
these concrete subclasses are what you need to instantiate and add to your instance.
|
||||
|
||||
To configure a QuartzScheduler, you can add a `Properties` object to the `QuartzSchedulerMetaData` object.
|
||||
See https://www.quartz-scheduler.org/documentation/[Quartz's documentation] for available configuration properties.
|
||||
|
||||
[source,java]
|
||||
.Defining SchedulerMetaData
|
||||
----
|
||||
qInstance.addScheduler(new SimpleSchedulerMetaData().withName("mySimpleScheduler"));
|
||||
|
||||
qInstance.addScheduler(new QuartzSchedulerMetaData()
|
||||
.withName("myQuartzScheduler")
|
||||
.withProperties(myQuartzProperties);
|
||||
----
|
||||
|
||||
=== SchedulableTypes
|
||||
The types of jobs which can be scheduled in a QQQ application are defined in the `QInstance` by
|
||||
instances of the `SchedulableType` meta-data class.
|
||||
These objects contain a name, along with a `QCodeReference` to the `runner`,
|
||||
which must be a class that implements the `SchedulableRunner` interface.
|
||||
|
||||
By default, (in the `QInstanceEnricher`), QQQ will make 3 `SchedulableType` options available:
|
||||
|
||||
* `PROCESS` - Any Process defined in the `QInstance` can be scheduled.
|
||||
* `QUEUE_PROCESSOR` - A Queue defined in the `QInstance`, which requires polling (e.g., SQS), can be scheduled.
|
||||
* `TABLE_AUTOMATIONS` - A Table in the `QInstance`, with `AutomationDetails` referring to an
|
||||
AutomationProvider which requires polling, can be scheduled.
|
||||
|
||||
If an application only wants to use a subset of these `SchedulableType` options,
|
||||
or to add custom `SchedulableType` options,
|
||||
the `QInstance` will need to have 1 or more `SchedulableType` objects in it before the `QInstanceEnricher` runs.
|
||||
|
||||
=== User-defined Scheduled Jobs
|
||||
To allow users to schedule jobs (rather than using programmer-defined schedules (in meta-data)),
|
||||
you can add a set of tables to your `QInstance`, using the `ScheduledJobsMetaDataProvider` class:
|
||||
|
||||
[source,java]
|
||||
.Adding the ScheduledJob tables and related meta-data to a QInstance
|
||||
----
|
||||
new ScheduledJobsMetaDataProvider().defineAll(
|
||||
qInstance, backendName, table -> tableEnricher(table));
|
||||
----
|
||||
|
||||
This meta-data provider adds a "scheduledJob" and "scheduledJobParameter" table, along with
|
||||
some PossibleValueSources.
|
||||
These tables include post-action customizers, which manage (re-, un-) scheduling jobs based on
|
||||
changes made to records in this these tables.
|
||||
|
||||
Also, when `QScheduleManager` is started, it will query these tables,and will schedule jobs as defined therein.
|
||||
|
||||
_You can use a mix of user-defined and meta-data defined scheduled jobs in your instance.
|
||||
However, if a ScheduledJob record references a process, queue, or table automation with a
|
||||
meta-data defined schedule, the ScheduledJob will NOT be started by ScheduleManager --
|
||||
rather, the meta-data definition will "win"._
|
||||
|
||||
[source,sql]
|
||||
.Example of inserting scheduled jobs records directly into an SQL backend
|
||||
----
|
||||
-- A process:
|
||||
INSERT INTO scheduled_job (label, scheduler_name, cron_expression, cron_time_zone_id, repeat_seconds, type, is_active) VALUES
|
||||
('myProcess', 'QuartzScheduler', null, null, 300, 'PROCESS', 1);
|
||||
INSERT INTO scheduled_job_parameter (scheduled_job_id, `key`, value) VALUES
|
||||
((SELECT id FROM scheduled_job WHERE label = 'myProcess'), 'processName', 'myProcess');
|
||||
|
||||
-- A table's insert & update automations:
|
||||
INSERT INTO scheduled_job (label, scheduler_name, cron_expression, cron_time_zone_id, repeat_seconds, type, is_active) VALUES
|
||||
('myTable.PENDING_INSERT_AUTOMATIONS', 'QuartzScheduler', null, null, 15, 'TABLE_AUTOMATIONS', 1),
|
||||
('myTable.PENDING_UPDATE_AUTOMATIONS', 'QuartzScheduler', null, null, 15, 'TABLE_AUTOMATIONS', 1);
|
||||
INSERT INTO scheduled_job_parameter (scheduled_job_id, `key`, value) VALUES
|
||||
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_INSERT_AUTOMATIONS'), 'tableName', 'myTable'),
|
||||
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_INSERT_AUTOMATIONS'), 'automationStatus', 'PENDING_INSERT_AUTOMATIONS'),
|
||||
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_UPDATE_AUTOMATIONS'), 'tableName', 'myTable'),
|
||||
((SELECT id FROM scheduled_job WHERE label = 'myTable.PENDING_UPDATE_AUTOMATIONS'), 'automationStatus', 'PENDING_UPDATE_AUTOMATIONS');
|
||||
|
||||
-- A queue processor:
|
||||
INSERT INTO scheduled_job (label, scheduler_name, cron_expression, cron_time_zone_id, repeat_seconds, type, is_active) VALUES
|
||||
('mySqsQueue', 'QuartzScheduler', null, null, 60, 'QUEUE_PROCESSOR', 1);
|
||||
INSERT INTO scheduled_job_parameter (scheduled_job_id, `key`, value) VALUES
|
||||
((SELECT id FROM scheduled_job WHERE label = 'mySqsQueue'), 'queueName', 'mySqsQueue');
|
||||
----
|
||||
|
||||
=== Running Scheduled Jobs
|
||||
In a server running QQQ, if you wish to start running scheduled jobs, you need to initialize
|
||||
the `QScheduleManger` singleton class, then call its `start()` method.
|
||||
|
||||
Note that internally, this class will check for a system property of `qqq.scheduleManager.enabled`
|
||||
or an environment variable of `QQQ_SCHEDULE_MANAGER_ENABLED`, and if the first value found is
|
||||
`"false"`, then the scheduler will not actually run its jobs (but, in the case of the `QuartzSchdeuler`,
|
||||
it will be available for managing scheduled jobs).
|
||||
|
||||
The method `QScheduleManager.initInstance` requires 2 parameters: Your `QInstance`, and a
|
||||
`Supplier<QSession>` lambda, which returns the session that will be used for scheduled jobs when they
|
||||
are executed.
|
||||
|
||||
[source,java]
|
||||
.Starting the Schedule Manager service
|
||||
----
|
||||
QScheduleManager.initInstance(qInstance, () -> systemUserSession).start();
|
||||
----
|
||||
|
||||
=== Examples
|
||||
[source,java]
|
||||
.Attach a schedule in meta-data to a Process
|
||||
----
|
||||
QProcessMetaData myProcess = new QProcessMetaData()
|
||||
// ...
|
||||
.withSchedule(new QScheduleMetaData()
|
||||
.withSchedulerName("myScheduler")
|
||||
.withDescription("Run myProcess every five minutes")
|
||||
.withRepeatSeconds(300))
|
||||
----
|
||||
|
272
docs/utilities/RecordLookupHelper.adoc
Normal file
272
docs/utilities/RecordLookupHelper.adoc
Normal file
@ -0,0 +1,272 @@
|
||||
== RecordLookupHelper
|
||||
include::../variables.adoc[]
|
||||
|
||||
`RecordLookupHelper` is a utility class that exists to help you... lookup records :)
|
||||
|
||||
OK, I'll try to give a little more context:
|
||||
|
||||
=== Motivation 1: Performance
|
||||
One of the most significant performance optimizations that the team behind QQQ has found time and time again,
|
||||
is to minimize the number of times you have to perform an I/O operation.
|
||||
To just say it more plainly:
|
||||
Make fewer calls to your database (or other backend).
|
||||
|
||||
This is part of why the DML actions in QQQ (InsertAction, UpdateAction, DeleteAction) are all written to work on multiple records:
|
||||
If you've got to insert 1,000 records, the performance difference between doing that as 1,000 SQL INSERT statements vs. just 1 statement cannot be overstated.
|
||||
|
||||
Similarly then, for looking up records:
|
||||
If we can do 1 round-trip to the database backend - that is - 1 query to fetch _n_ records,
|
||||
then in almost all cases it will be significantly faster than doing _n_ queries, one-by-one, for those _n_ records.
|
||||
|
||||
The primary reason why `RecordLookupHelper` exists is to help you cut down on the number of times you have to make a round-trip to a backend data store to fetch records within a process.
|
||||
|
||||
[sidebar]
|
||||
This basically is version of caching, isn't it?
|
||||
Take a set of data from "far away" (e.g., database), and bring it "closer" (local or instance variables), for faster access.
|
||||
So we may describe this as a "cache" through the rest of this document.
|
||||
|
||||
=== Motivation 2: Convenience
|
||||
|
||||
So, given that one wants to try to minimize the number of queries being executed to look up data in a QQQ processes,
|
||||
one can certainly do this "by-hand" in each process that they write.
|
||||
|
||||
Doing this kind of record caching in a QQQ Process `BackendStep` may be done as:
|
||||
|
||||
* Adding a `Map<Integer, QRecord>` as a field in your class.
|
||||
* Setting up and running a `QueryAction`, with a filter based on the collection of the keys you need to look up, then iterating over (or streaming) the results into the map field.
|
||||
* Getting values out of the map when you need to use them (dealing with missing values as needed).
|
||||
|
||||
That's not so bad, but, it does get a little verbose, especially if you're going to have several such caches in your class.
|
||||
|
||||
As such, the second reason that `RecordLookupHelper` exists, is to be a reusable and convenient way to do this kind of optimization,
|
||||
by providing methods to perform the bulk query & map building operation described above,
|
||||
while also providing some convenient methods for accessing such data after it's been fetched.
|
||||
In addition, a single instance of `RecordLookupHelper` can provide this service for multiple tables at once
|
||||
(e.g., so you don't need to add a field to your class for each type of data that you're trying to cache).
|
||||
|
||||
=== Use Cases
|
||||
==== Preload records, then access them
|
||||
Scenario:
|
||||
|
||||
* We're writing a process `BackendStep` that uses `shipment` records as input.
|
||||
* We need to know the `order` record associated with each `shipment` (via an `orderId` foreign key), for some business logic that isn't germaine to the explanation of `RecordLookupHelper`.
|
||||
* We also to access some field on the `shippingPartner` record assigned to each `shipment`.
|
||||
** Note that here, the `shipment` table has a `partnerCode` field, which relates to the `code` unique-key in the `shippingPartner` table.
|
||||
** It's also worth mentioning, we only have a handful of `shippingPartner` records in our database, and we never expect to have very many more than that.
|
||||
|
||||
[source,java]
|
||||
.Example of a process step using a RecordLookupHelper to preload records
|
||||
----
|
||||
public class MyShipmentProcessStep implements BackendStep
|
||||
{
|
||||
// Add a new RecordLookupHelper field, which will "cache" both orders and shippingPartners
|
||||
private RecordLookupHelper recordLookupHelper = new RecordLookupHelper();
|
||||
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException;
|
||||
{
|
||||
// lookup the full shippingPartner table (because it's cheap & easy to do so)
|
||||
// use the partner's "code" as the key field (e.g,. they key in the helper's internal map).
|
||||
recordLookupHelper.preloadRecords("shippingPartner", "code");
|
||||
|
||||
// get all of the orderIds from the input shipments
|
||||
List<Serializable> orderIds = input.getRecords().stream()
|
||||
.map(r -> r.getValue("id")).toList();
|
||||
|
||||
// fetch the orders related to by these shipments
|
||||
recordLookupHelper.preloadRecords("order", "id", orderIds);
|
||||
|
||||
for(QRecord shipment : input.getRecords())
|
||||
{
|
||||
// get someConfigField from the shippingPartner assigned to the shipment
|
||||
Boolean someConfig = recordLookupHelper.getRecordValue("shippingPartner", "someConfigField", "code", shipment.getValue("partnerCode"));
|
||||
|
||||
// get the order record assigned to the shipment
|
||||
QRecord order = recordLookupHelper.getRecordByKey("order", "id", shipment.getValue("orderId"));
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
==== Lazy fetching records
|
||||
Scenario:
|
||||
|
||||
* We have a `BackendStep` that is taking in `purchaseOrderHeader` records, from an API partner.
|
||||
* For each record, we need to make an API call to the partner to fetch the `purchaseOrderLine` records under that header.
|
||||
** In this contrived example, the partner's API forces us to do these lookups order-by-order...
|
||||
* Each `purchaseOrderLine` that we fetch will have a `sku` on it - a reference to our `item` table.
|
||||
** We need to look up each `item` to apply some business logic.
|
||||
** We assume there are very many item records in the backend database, so we don't want to pre-load the full table.
|
||||
Also, we don't know what `sku` values we will need until we fetch the `purchaseOrderLine`.
|
||||
|
||||
This is a situation where we can use `RecordLookupHelper` to lazily fetch the `item` records as we discover them,
|
||||
and it will take care of not re-fetching ones that it has already loaded.
|
||||
|
||||
[source,java]
|
||||
.Example of a process step using a RecordLookupHelper to lazy fetch records
|
||||
----
|
||||
public class MyPurchaseOrderProcessStep implements BackendStep
|
||||
{
|
||||
// Add a new RecordLookupHelper field, which will "cache" lazy-loaded item records
|
||||
private RecordLookupHelper recordLookupHelper = new RecordLookupHelper();
|
||||
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException;
|
||||
{
|
||||
for(QRecord poHeader : input.getRecords())
|
||||
{
|
||||
// fetch the lines under the header
|
||||
Serializable poNo = poHeader.getValue("poNo");
|
||||
List<QRecord> poLines = new QueryAction().execute(new QueryInput("purchaseOrderLine")
|
||||
.withFilter(new QQueryFilter(new QFilterCriteria("poNo", EQUALS, poNo))));
|
||||
|
||||
for(QRecord poLine : poLines)
|
||||
{
|
||||
// use recordLookupHelper to lazy-load item records by SKU.
|
||||
QRecord item = recordLookupHelper.getRecordByKey("item", "sku", poLine.getValue("sku"));
|
||||
|
||||
// business logic related to item performed here.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In this example, we will be doing exactly 1 query on the `item` table for each unique `sku` that is found across all of the `poLine` records we process.
|
||||
That is to say, if the same `sku` appears on only 1 `poLine`, or if it appears on 100 `poLines`, we will still only query once for that `sku`.
|
||||
|
||||
A slight tweak could be made to the example above, to make 1 `item` table lookup for each `poHeader` record:
|
||||
|
||||
[source,java]
|
||||
.Tweaked example doing 1 item lookup per poLine
|
||||
----
|
||||
// continuing from above, after the List<QRecord> poLines has been built
|
||||
|
||||
// get all of the skus from the lines
|
||||
List<Serializable> skus = poLines.stream().map(r -> r.getValue("sku")).toList();
|
||||
|
||||
// preload the items for the skus
|
||||
recordLookupHelper.preloadRecords("item", "sku", new QQueryFilter(new QFilterCriteria("sku", IN, skus)));
|
||||
|
||||
for(QRecord poLine : poLines)
|
||||
{
|
||||
// get the items from the helper
|
||||
QRecord item = recordLookupHelper.getRecordByKey("item", "sku", poLine.getValue("sku"));
|
||||
|
||||
----
|
||||
|
||||
In this example, we've made a trade-off: We will query the `item` table exactly 1 time for each `poHeader` that we process.
|
||||
However, if the same `sku` is on every PO that we process, we will end up fetching it multiple times.
|
||||
|
||||
This could end up being better or worse than the previous example, depending on the distribution of the data we are dealing with.
|
||||
|
||||
A further tweak, a hybrid approach, could potentially reap the benefits of both of these examples (at the tradeoff of, more code, more complexity):
|
||||
|
||||
[source,java]
|
||||
.Tweaked example doing 1 item lookup per poLine, but only for not-previously-encountered skus
|
||||
----
|
||||
// Critically - we must tell our recordLookupHelper to NOT do any one-off lookups in this table
|
||||
recordLookupHelper.setMayNotDoOneOffLookups("item", "sku");
|
||||
|
||||
// continuing from above, after the List<QRecord> poLines has been built
|
||||
|
||||
// get all of the skus from the lines
|
||||
List<Serializable> skus = poLines.stream().map(r -> r.getValue("sku")).toList();
|
||||
|
||||
// determine which skus have not yet been loaded - e.g., they are not in the recordLookupHelper.
|
||||
// this is why we needed to tell it above not to do one-off lookups; else it would lazy-load each sku here.
|
||||
List<Serializable> skusToLoad = new ArrayList<>();
|
||||
for(Serializable sku : skus)
|
||||
{
|
||||
if(recordLookupHelper.getRecordByKey("item", "sku", sku) == null)
|
||||
{
|
||||
skusToLoad.add(sku);
|
||||
}
|
||||
}
|
||||
|
||||
// preload the item records for any skus that are still needed
|
||||
if(!skusToLoad.isEmpty())
|
||||
{
|
||||
recordLookupHelper.preloadRecords("item", "sku",
|
||||
new QQueryFilter(new QFilterCriteria("sku", IN, skusToLoad)));
|
||||
}
|
||||
|
||||
// continue as above
|
||||
|
||||
----
|
||||
|
||||
In this example, we will start by querying the `item` table once for each `poHeader`, but,
|
||||
if we eventually encounter a PO where all of its `skus` have already been loaded, then we may be able to avoid any `item` queries for such a PO.
|
||||
|
||||
=== Implementation Details
|
||||
|
||||
* Internally, an instance of `RecordLookupHelper` maintains a number of `Maps`,
|
||||
with QQQ table names and field names as keys.
|
||||
* The accessing/lazy-fetching methods (e.g., any method whose name starts with `getRecord`)
|
||||
all begin by looking in these internal maps for the `tableName` and `keyFieldName` that they take as parameters.
|
||||
** If they find an entry in the maps, then it is used for producing a return value.
|
||||
** If they do not find an entry, then they will perform the a `QueryAction`,
|
||||
to try to fetch the requested record from the table's backend.
|
||||
*** Unless the `setMayNotDoOneOffLookups` method has been called for the `(tableName, keyFieldName)` pair.
|
||||
|
||||
=== Full API
|
||||
|
||||
==== Methods for accessing and lazy-fetching
|
||||
* `getRecordByKey(String tableName, String keyFieldName, Serializable key)`
|
||||
|
||||
Get a `QRecord` from `tableName`, where `keyFieldName` = `key`.
|
||||
|
||||
* `getRecordValue(String tableName, String requestedField, String keyFieldName, Serializable key)`
|
||||
|
||||
Get the field `requestedField` from the record in `tableName`, where `keyFieldName` = `key`, as a `Serializable`.
|
||||
If the record is not found, `null` is returned.
|
||||
|
||||
* `getRecordValue(String tableName, String requestedField, String keyFieldName, Serializable key, Class<T> type)`
|
||||
|
||||
Get the field `requestedField` from the record in `tableName`, where `keyFieldName` = `key`, as an instance of `type`.
|
||||
If the record is not found, `null` is returned.
|
||||
|
||||
* `getRecordId(String tableName, String keyFieldName, Serializable key)`
|
||||
|
||||
Get the primary key of the record in `tableName`, where `keyFieldName` = `key`, as a `Serializable`.
|
||||
If the record is not found, `null` is returned.
|
||||
|
||||
* `getRecordId(String tableName, String keyFieldName, Serializable key, Class<T> type)`
|
||||
|
||||
Get the primary key of the record in `tableName`, where `keyFieldName` = `key`, as an instance of `type`.
|
||||
If the record is not found, `null` is returned.
|
||||
|
||||
* `getRecordByUniqueKey(String tableName, Map<String, Serializable> uniqueKey)`
|
||||
|
||||
Get a `QRecord` from `tableName`, where the record matches the field/value pairs in `uniqueKey`.
|
||||
|
||||
_Note: this method does not use the same internal map as the rest of the class.
|
||||
As such, it does not take advantage of any data fetched via the preload methods.
|
||||
It is only used for caching lazy-fetches._
|
||||
|
||||
==== Methods for preloading
|
||||
* `preloadRecords(String tableName, String keyFieldName)`
|
||||
|
||||
Query for all records from `tableName`, storing them in an internal map keyed by the field `keyFieldName`.
|
||||
|
||||
* `preloadRecords(String tableName, String keyFieldName, QQueryFilter filter)`
|
||||
|
||||
Query for records matching `filter` from `tableName`,
|
||||
storing them in an internal map keyed by the field `keyFieldName`.
|
||||
|
||||
* `preloadRecords(String tableName, String keyFieldName, List<Serializable> inList)`
|
||||
|
||||
Query for records with the field `keyFieldName` having a value in `inList` from `tableName`,
|
||||
storing them in an internal map keyed by the field `keyFieldName`.
|
||||
|
||||
==== Config Methods
|
||||
* `setMayNotDoOneOffLookups(String tableName, String fieldName)`
|
||||
|
||||
For cases where you know that you have preloaded records for `tableName`, keyed by `fieldName`,
|
||||
and you know that some of the keys may not have been found,
|
||||
so you want to avoid doing a query when a missed key is found in one of the `getRecord...` methods,
|
||||
then if you call this method, an internal flag is set, which will prevent any such one-off lookups.
|
||||
|
||||
In other words, if this method has been called for a `(tableName, fieldName)` pair,
|
||||
then the `getRecord...` methods will only look in the internal map for records,
|
||||
and no queries will be performed to look for records.
|
48
pom.xml
48
pom.xml
@ -80,12 +80,12 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<version>2.17.1</version>
|
||||
<version>2.23.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>2.17.1</version>
|
||||
<version>2.23.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
@ -330,28 +330,28 @@ fi
|
||||
|
||||
<!-- mvn javadoc:aggregate -->
|
||||
<reporting>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.2</version>
|
||||
<reportSets>
|
||||
<reportSet>
|
||||
<id>aggregate</id>
|
||||
<inherited>false</inherited>
|
||||
<reports>
|
||||
<report>aggregate</report>
|
||||
</reports>
|
||||
</reportSet>
|
||||
<reportSet>
|
||||
<id>default</id>
|
||||
<reports>
|
||||
<report>javadoc</report>
|
||||
</reports>
|
||||
</reportSet>
|
||||
</reportSets>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.6.2</version>
|
||||
<reportSets>
|
||||
<reportSet>
|
||||
<id>aggregate</id>
|
||||
<inherited>false</inherited>
|
||||
<reports>
|
||||
<report>aggregate</report>
|
||||
</reports>
|
||||
</reportSet>
|
||||
<reportSet>
|
||||
<id>default</id>
|
||||
<reports>
|
||||
<report>javadoc</report>
|
||||
</reports>
|
||||
</reportSet>
|
||||
</reportSets>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</reporting>
|
||||
|
||||
<repositories>
|
||||
|
@ -102,6 +102,16 @@
|
||||
<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>
|
||||
@ -163,6 +173,19 @@
|
||||
<version>1.12.321</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.quartz-scheduler</groupId>
|
||||
<artifactId>quartz</artifactId>
|
||||
<version>2.3.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Many of the deps we bring in use slf4j. This dep maps slf4j to our logger, log4j -->
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j-impl</artifactId>
|
||||
<version>2.23.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Common deps for all qqq modules -->
|
||||
<dependency>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
@ -159,7 +159,7 @@ public class AsyncJobManager
|
||||
private <T extends Serializable> T runAsyncJob(String jobName, AsyncJob<T> asyncJob, UUIDAndTypeStateKey uuidAndTypeStateKey, AsyncJobStatus asyncJobStatus)
|
||||
{
|
||||
String originalThreadName = Thread.currentThread().getName();
|
||||
Thread.currentThread().setName("Job:" + jobName + ":" + uuidAndTypeStateKey.getUuid().toString().substring(0, 8));
|
||||
Thread.currentThread().setName("Job:" + jobName);
|
||||
try
|
||||
{
|
||||
LOG.debug("Starting job " + uuidAndTypeStateKey.getUuid());
|
||||
@ -169,17 +169,24 @@ public class AsyncJobManager
|
||||
LOG.debug("Completed job " + uuidAndTypeStateKey.getUuid());
|
||||
return (result);
|
||||
}
|
||||
catch(Exception e)
|
||||
catch(Throwable t)
|
||||
{
|
||||
asyncJobStatus.setState(AsyncJobState.ERROR);
|
||||
asyncJobStatus.setCaughtException(e);
|
||||
if(t instanceof Exception e)
|
||||
{
|
||||
asyncJobStatus.setCaughtException(e);
|
||||
}
|
||||
else
|
||||
{
|
||||
asyncJobStatus.setCaughtException(new QException("Caught throwable", t));
|
||||
}
|
||||
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// if user facing, just log an info, warn otherwise //
|
||||
//////////////////////////////////////////////////////
|
||||
LOG.log((e instanceof QUserFacingException) ? Level.INFO : Level.WARN, "Job ended with an exception", e, logPair("jobId", uuidAndTypeStateKey.getUuid()));
|
||||
throw (new CompletionException(e));
|
||||
LOG.log((t instanceof QUserFacingException) ? Level.INFO : Level.WARN, "Job ended with an exception", t, logPair("jobId", uuidAndTypeStateKey.getUuid()));
|
||||
throw (new CompletionException(t));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -83,6 +83,15 @@ public class AsyncRecordPipeLoop
|
||||
long jobStartTime = System.currentTimeMillis();
|
||||
boolean everCalledConsumer = false;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// in case the pipe capacity has been made very small (useful in tests!), //
|
||||
// then make the minRecordsToConsume match it. //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
if(recordPipe.getCapacity() < minRecordsToConsume)
|
||||
{
|
||||
minRecordsToConsume = recordPipe.getCapacity();
|
||||
}
|
||||
|
||||
while(jobState.equals(AsyncJobState.RUNNING))
|
||||
{
|
||||
if(recordPipe.countAvailableRecords() < minRecordsToConsume)
|
||||
|
@ -295,7 +295,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error performing an audit", e);
|
||||
LOG.warn("Error performing an audit", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,7 +187,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error performing DML audit", e, logPair("type", String.valueOf(dmlType)), logPair("table", table.getName()));
|
||||
LOG.warn("Error performing DML audit", e, logPair("type", String.valueOf(dmlType)), logPair("table", table.getName()));
|
||||
}
|
||||
|
||||
return (output);
|
||||
|
@ -28,6 +28,7 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
|
||||
@ -60,6 +61,8 @@ import com.kingsrook.qqq.backend.core.model.automation.TableTrigger;
|
||||
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.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
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.model.metadata.tables.automation.AutomationStatusTrackingType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
|
||||
@ -129,6 +132,11 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
*******************************************************************************/
|
||||
String tableName();
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
QTableAutomationDetails tableAutomationDetails();
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -140,7 +148,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
/*******************************************************************************
|
||||
** Wrapper for a pair of (tableName, automationStatus)
|
||||
*******************************************************************************/
|
||||
public record TableActions(String tableName, AutomationStatus status) implements TableActionsInterface
|
||||
public record TableActions(String tableName, QTableAutomationDetails tableAutomationDetails, AutomationStatus status) implements TableActionsInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -156,7 +164,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
** extended version of TableAction, for sharding use-case - adds the shard
|
||||
** details.
|
||||
*******************************************************************************/
|
||||
public record ShardedTableActions(String tableName, AutomationStatus status, String shardByFieldName, Serializable shardValue, String shardLabel) implements TableActionsInterface
|
||||
public record ShardedTableActions(String tableName, QTableAutomationDetails tableAutomationDetails, AutomationStatus status, String shardByFieldName, Serializable shardValue, String shardLabel) implements TableActionsInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -195,8 +203,8 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
{
|
||||
Serializable shardId = record.getValue(automationDetails.getShardIdFieldName());
|
||||
String label = record.getValueString(automationDetails.getShardLabelFieldName());
|
||||
tableActionList.add(new ShardedTableActions(table.getName(), AutomationStatus.PENDING_INSERT_AUTOMATIONS, automationDetails.getShardByFieldName(), shardId, label));
|
||||
tableActionList.add(new ShardedTableActions(table.getName(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS, automationDetails.getShardByFieldName(), shardId, label));
|
||||
tableActionList.add(new ShardedTableActions(table.getName(), automationDetails, AutomationStatus.PENDING_INSERT_AUTOMATIONS, automationDetails.getShardByFieldName(), shardId, label));
|
||||
tableActionList.add(new ShardedTableActions(table.getName(), automationDetails, AutomationStatus.PENDING_UPDATE_AUTOMATIONS, automationDetails.getShardByFieldName(), shardId, label));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -206,11 +214,11 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// for non-sharded, we just need tabler name & automation status //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
tableActionList.add(new TableActions(table.getName(), AutomationStatus.PENDING_INSERT_AUTOMATIONS));
|
||||
tableActionList.add(new TableActions(table.getName(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS));
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// for non-sharded, we just need table name & automation status //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
tableActionList.add(new TableActions(table.getName(), automationDetails, AutomationStatus.PENDING_INSERT_AUTOMATIONS));
|
||||
tableActionList.add(new TableActions(table.getName(), automationDetails, AutomationStatus.PENDING_UPDATE_AUTOMATIONS));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,7 +264,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error running automations", e);
|
||||
LOG.warn("Error running automations", e, logPair("tableName", tableActions.tableName()), logPair("status", tableActions.status()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -300,7 +308,9 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType();
|
||||
if(AutomationStatusTrackingType.FIELD_IN_TABLE.equals(statusTrackingType))
|
||||
{
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId()))));
|
||||
QQueryFilter filter = new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId())));
|
||||
addOrderByToQueryFilter(table, automationStatus, filter);
|
||||
queryInput.setFilter(filter);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -329,6 +339,38 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static void addOrderByToQueryFilter(QTableMetaData table, AutomationStatus automationStatus, QQueryFilter filter)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// look for a field in the table with either create-date or modify-date behavior, //
|
||||
// based on if doing insert or update automations //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
DynamicDefaultValueBehavior dynamicDefaultValueBehavior = automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS) ? DynamicDefaultValueBehavior.CREATE_DATE : DynamicDefaultValueBehavior.MODIFY_DATE;
|
||||
Optional<QFieldMetaData> field = table.getFields().values().stream()
|
||||
.filter(f -> dynamicDefaultValueBehavior.equals(f.getBehaviorOrDefault(QContext.getQInstance(), DynamicDefaultValueBehavior.class)))
|
||||
.findFirst();
|
||||
|
||||
if(field.isPresent())
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// if a create/modify date field was found, order by it (ascending) //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
filter.addOrderBy(new QFilterOrderBy(field.get().getName()));
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////
|
||||
// else, order by the primary key //
|
||||
////////////////////////////////////
|
||||
filter.addOrderBy(new QFilterOrderBy(table.getPrimaryKeyField()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get the actions to run against a table in an automation status. both from
|
||||
** metaData and tableTriggers/data.
|
||||
@ -457,13 +499,15 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
////////////////////////////////////////
|
||||
// update status on all these records //
|
||||
////////////////////////////////////////
|
||||
if(anyActionsFailed)
|
||||
AutomationStatus statusToUpdateTo = anyActionsFailed ? pendingToFailedStatusMap.get(automationStatus) : AutomationStatus.OK;
|
||||
try
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, pendingToFailedStatusMap.get(automationStatus), null);
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, statusToUpdateTo, null);
|
||||
}
|
||||
else
|
||||
catch(Exception e)
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, AutomationStatus.OK, null);
|
||||
LOG.warn("Error updating automationStatus after running automations", logPair("tableName", table), logPair("count", records.size()), logPair("status", statusToUpdateTo));
|
||||
throw (e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -493,7 +537,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
|
||||
LOG.warn("Caught exception processing automations", e, logPair("tableName", table), logPair("action", action.getName()));
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
@ -47,12 +47,24 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
** records that the delete action marked in error - the user might want to do
|
||||
** something special with them (idk, try some other way to delete them?)
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractPostDeleteCustomizer
|
||||
public abstract class AbstractPostDeleteCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
protected DeleteInput deleteInput;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
|
||||
{
|
||||
this.deleteInput = deleteInput;
|
||||
return apply(records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -42,12 +42,24 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
**
|
||||
** Note that the full insertInput is available as a field in this class.
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractPostInsertCustomizer
|
||||
public abstract class AbstractPostInsertCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
protected InsertInput insertInput;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
|
||||
{
|
||||
this.insertInput = insertInput;
|
||||
return (apply(records));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -23,16 +23,29 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractPostQueryCustomizer
|
||||
public abstract class AbstractPostQueryCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
protected AbstractTableActionInput input;
|
||||
protected QueryOrGetInputInterface input;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
|
||||
{
|
||||
input = queryInput;
|
||||
return apply(records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -47,7 +60,7 @@ public abstract class AbstractPostQueryCustomizer
|
||||
** Getter for input
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AbstractTableActionInput getInput()
|
||||
public QueryOrGetInputInterface getInput()
|
||||
{
|
||||
return (input);
|
||||
}
|
||||
@ -58,7 +71,7 @@ public abstract class AbstractPostQueryCustomizer
|
||||
** Setter for input
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setInput(AbstractTableActionInput input)
|
||||
public void setInput(QueryOrGetInputInterface input)
|
||||
{
|
||||
this.input = input;
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@ -48,7 +49,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
** available (if the backend supports it) - both as a list (`getOldRecordList`)
|
||||
** and as a memoized (by this class) map of primaryKey to record (`getOldRecordMap`).
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractPostUpdateCustomizer
|
||||
public abstract class AbstractPostUpdateCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
protected UpdateInput updateInput;
|
||||
protected List<QRecord> oldRecordList;
|
||||
@ -57,6 +58,19 @@ public abstract class AbstractPostUpdateCustomizer
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
this.updateInput = updateInput;
|
||||
this.oldRecordList = oldRecordList.orElse(null);
|
||||
return apply(records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -50,7 +50,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
** Note that the full deleteInput is available as a field in this class.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractPreDeleteCustomizer
|
||||
public abstract class AbstractPreDeleteCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
protected DeleteInput deleteInput;
|
||||
|
||||
@ -58,6 +58,19 @@ public abstract class AbstractPreDeleteCustomizer
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preDelete(DeleteInput deleteInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
this.deleteInput = deleteInput;
|
||||
this.isPreview = isPreview;
|
||||
return apply(records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -47,7 +47,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
**
|
||||
** Note that the full insertInput is available as a field in this class.
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractPreInsertCustomizer
|
||||
public abstract class AbstractPreInsertCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
protected InsertInput insertInput;
|
||||
|
||||
@ -70,6 +70,30 @@ public abstract class AbstractPreInsertCustomizer
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
this.insertInput = insertInput;
|
||||
this.isPreview = isPreview;
|
||||
return (apply(records));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public WhenToRun whenToRunPreInsert(InsertInput insertInput, boolean isPreview)
|
||||
{
|
||||
return getWhenToRun();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -26,6 +26,7 @@ import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@ -53,7 +54,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
** available (if the backend supports it) - both as a list (`getOldRecordList`)
|
||||
** and as a memoized (by this class) map of primaryKey to record (`getOldRecordMap`).
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractPreUpdateCustomizer
|
||||
public abstract class AbstractPreUpdateCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
protected UpdateInput updateInput;
|
||||
protected List<QRecord> oldRecordList;
|
||||
@ -63,6 +64,20 @@ public abstract class AbstractPreUpdateCustomizer
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
this.updateInput = updateInput;
|
||||
this.isPreview = isPreview;
|
||||
this.oldRecordList = oldRecordList.orElse(null);
|
||||
return apply(records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
|
||||
@ -34,42 +35,35 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility to load code for running QQQ customizers.
|
||||
**
|
||||
** TODO - redo all to go through method that memoizes class & constructor
|
||||
** lookup. That memoziation causes 1,000,000 such calls to go from ~500ms
|
||||
** to ~100ms.
|
||||
*******************************************************************************/
|
||||
public class QCodeLoader
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QCodeLoader.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T, R> Optional<Function<T, R>> getTableCustomizerFunction(QTableMetaData table, String customizerName)
|
||||
{
|
||||
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
|
||||
if(codeReference.isPresent())
|
||||
{
|
||||
return (Optional.ofNullable(QCodeLoader.getFunction(codeReference.get())));
|
||||
}
|
||||
return (Optional.empty());
|
||||
}
|
||||
private static Memoization<String, Constructor<?>> constructorMemoization = new Memoization<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T> Optional<T> getTableCustomizer(Class<T> expectedClass, QTableMetaData table, String customizerName)
|
||||
public static Optional<TableCustomizerInterface> getTableCustomizer(QTableMetaData table, String customizerName)
|
||||
{
|
||||
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
|
||||
if(codeReference.isPresent())
|
||||
{
|
||||
return (Optional.ofNullable(QCodeLoader.getAdHoc(expectedClass, codeReference.get())));
|
||||
return (Optional.ofNullable(QCodeLoader.getAdHoc(TableCustomizerInterface.class, codeReference.get())));
|
||||
}
|
||||
return (Optional.empty());
|
||||
}
|
||||
@ -175,8 +169,21 @@ public class QCodeLoader
|
||||
|
||||
try
|
||||
{
|
||||
Class<?> customizerClass = Class.forName(codeReference.getName());
|
||||
return ((T) customizerClass.getConstructor().newInstance());
|
||||
Optional<Constructor<?>> constructor = constructorMemoization.getResultThrowing(codeReference.getName(), (UnsafeFunction<String, Constructor<?>, Exception>) s ->
|
||||
{
|
||||
Class<?> customizerClass = Class.forName(codeReference.getName());
|
||||
return customizerClass.getConstructor();
|
||||
});
|
||||
|
||||
if(constructor.isPresent())
|
||||
{
|
||||
return ((T) constructor.get().newInstance());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.error("Could not get constructor for code reference", logPair("codeReference", codeReference));
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
|
@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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.customizers;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Common interface used by all (core) TableCustomizer types (e.g., post-query,
|
||||
** and {pre,post}-{insert,update,delete}.
|
||||
**
|
||||
** Note that the abstract-base classes for each action still exist, though have
|
||||
** been back-ported to be implementors of this interface. The action classes
|
||||
** will now expect this type, and call this type's methods.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface TableCustomizerInterface
|
||||
{
|
||||
QLogger LOG = QLogger.getLogger(TableCustomizerInterface.class);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** custom actions to run after a query (or get!) takes place.
|
||||
**
|
||||
*******************************************************************************/
|
||||
default List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
|
||||
{
|
||||
LOG.info("A default implementation of postQuery is running... Probably not expected!", logPair("tableName", queryInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** custom actions before an insert takes place.
|
||||
**
|
||||
** It's important for implementations to be aware of the isPreview field, which
|
||||
** is set to true when the code is running to give users advice, e.g., on a review
|
||||
** screen - vs. being false when the action is ACTUALLY happening. So, if you're doing
|
||||
** things like storing data, you don't want to do that if isPreview is true!!
|
||||
**
|
||||
** General implementation would be, to iterate over the records (the inputs to
|
||||
** the insert action), and look at their values:
|
||||
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
|
||||
** - possibly manipulating values (`setValue`)
|
||||
** - possibly throwing an exception - if you really don't want the insert operation to continue.
|
||||
** - doing "whatever else" you may want to do.
|
||||
** - returning the list of records (can be the input list) that you want to go on to the backend implementation class.
|
||||
*******************************************************************************/
|
||||
default List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
LOG.info("A default implementation of preInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default AbstractPreInsertCustomizer.WhenToRun whenToRunPreInsert(InsertInput insertInput, boolean isPreview)
|
||||
{
|
||||
return (AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** custom actions after an insert takes place.
|
||||
**
|
||||
** General implementation would be, to iterate over the records (the outputs of
|
||||
** the insert action), and look at their values:
|
||||
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
|
||||
** - possibly throwing an exception - though doing so won't stop the update, and instead
|
||||
** will just set a warning on all of the updated records...
|
||||
** - doing "whatever else" you may want to do.
|
||||
** - returning the list of records (can be the input list) that you want to go back to the caller.
|
||||
*******************************************************************************/
|
||||
default List<QRecord> postInsert(InsertInput insertInput, List<QRecord> records) throws QException
|
||||
{
|
||||
LOG.info("A default implementation of postInsert is running... Probably not expected!", logPair("tableName", insertInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** custom actions before an update takes place.
|
||||
**
|
||||
** It's important for implementations to be aware of the isPreview field, which
|
||||
** is set to true when the code is running to give users advice, e.g., on a review
|
||||
** screen - vs. being false when the action is ACTUALLY happening. So, if you're doing
|
||||
** things like storing data, you don't want to do that if isPreview is true!!
|
||||
**
|
||||
** General implementation would be, to iterate over the records (the inputs to
|
||||
** the update action), and look at their values:
|
||||
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
|
||||
** - possibly manipulating values (`setValue`)
|
||||
** - possibly throwing an exception - if you really don't want the update operation to continue.
|
||||
** - doing "whatever else" you may want to do.
|
||||
** - returning the list of records (can be the input list) that you want to go on to the backend implementation class.
|
||||
**
|
||||
** Note, "old records" (e.g., with values freshly fetched from the backend) will be
|
||||
** available (if the backend supports it)
|
||||
*******************************************************************************/
|
||||
default List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
LOG.info("A default implementation of preUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** custom actions after an update takes place.
|
||||
**
|
||||
** General implementation would be, to iterate over the records (the outputs of
|
||||
** the update action), and look at their values:
|
||||
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records?
|
||||
** - possibly throwing an exception - though doing so won't stop the update, and instead
|
||||
** will just set a warning on all of the updated records...
|
||||
** - doing "whatever else" you may want to do.
|
||||
** - returning the list of records (can be the input list) that you want to go back to the caller.
|
||||
**
|
||||
** Note, "old records" (e.g., with values freshly fetched from the backend) will be
|
||||
** available (if the backend supports it).
|
||||
*******************************************************************************/
|
||||
default List<QRecord> postUpdate(UpdateInput updateInput, List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
LOG.info("A default implementation of postUpdate is running... Probably not expected!", logPair("tableName", updateInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Custom actions before a delete takes place.
|
||||
**
|
||||
** It's important for implementations to be aware of the isPreview param, which
|
||||
** is set to true when the code is running to give users advice, e.g., on a review
|
||||
** screen - vs. being false when the action is ACTUALLY happening. So, if you're doing
|
||||
** things like storing data, you don't want to do that if isPreview is true!!
|
||||
**
|
||||
** General implementation would be, to iterate over the records (which the DeleteAction
|
||||
** would look up based on the inputs to the delete action), and look at their values:
|
||||
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
|
||||
** - possibly throwing an exception - if you really don't want the delete operation to continue.
|
||||
** - doing "whatever else" you may want to do.
|
||||
** - returning the list of records (can be the input list) - this is how errors
|
||||
** and warnings are propagated to the DeleteAction. Note that any records with
|
||||
** an error will NOT proceed to the backend's delete interface - but those with
|
||||
** warnings will.
|
||||
*******************************************************************************/
|
||||
default List<QRecord> preDelete(DeleteInput deleteInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
LOG.info("A default implementation of preDelete is running... Probably not expected!", logPair("tableName", deleteInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Custom actions after a delete takes place.
|
||||
**
|
||||
** General implementation would be, to iterate over the records (ones which didn't
|
||||
** have a delete error), and look at their values:
|
||||
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records?
|
||||
** - possibly throwing an exception - though doing so won't stop the delete, and instead
|
||||
** will just set a warning on all of the deleted records...
|
||||
** - doing "whatever else" you may want to do.
|
||||
** - returning the list of records (can be the input list) that you want to go back
|
||||
** to the caller - this is how errors and warnings are propagated .
|
||||
*******************************************************************************/
|
||||
default List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
|
||||
{
|
||||
LOG.info("A default implementation of postDelete is running... Probably not expected!", logPair("tableName", deleteInput.getTableName()));
|
||||
return (records);
|
||||
}
|
||||
|
||||
}
|
@ -29,13 +29,13 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
*******************************************************************************/
|
||||
public enum TableCustomizers
|
||||
{
|
||||
POST_QUERY_RECORD("postQueryRecord", AbstractPostQueryCustomizer.class),
|
||||
PRE_INSERT_RECORD("preInsertRecord", AbstractPreInsertCustomizer.class),
|
||||
POST_INSERT_RECORD("postInsertRecord", AbstractPostInsertCustomizer.class),
|
||||
PRE_UPDATE_RECORD("preUpdateRecord", AbstractPreUpdateCustomizer.class),
|
||||
POST_UPDATE_RECORD("postUpdateRecord", AbstractPostUpdateCustomizer.class),
|
||||
PRE_DELETE_RECORD("preDeleteRecord", AbstractPreDeleteCustomizer.class),
|
||||
POST_DELETE_RECORD("postDeleteRecord", AbstractPostDeleteCustomizer.class);
|
||||
POST_QUERY_RECORD("postQueryRecord", TableCustomizerInterface.class),
|
||||
PRE_INSERT_RECORD("preInsertRecord", TableCustomizerInterface.class),
|
||||
POST_INSERT_RECORD("postInsertRecord", TableCustomizerInterface.class),
|
||||
PRE_UPDATE_RECORD("preUpdateRecord", TableCustomizerInterface.class),
|
||||
POST_UPDATE_RECORD("postUpdateRecord", TableCustomizerInterface.class),
|
||||
PRE_DELETE_RECORD("preDeleteRecord", TableCustomizerInterface.class),
|
||||
POST_DELETE_RECORD("postDeleteRecord", TableCustomizerInterface.class);
|
||||
|
||||
|
||||
private final String role;
|
||||
|
@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
@ -176,11 +177,13 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
return (totalString);
|
||||
}
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return ("<a href='" + tablePath + "?filter=" + JsonUtils.toJson(filter) + "'>" + totalString + "</a>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -192,6 +195,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return;
|
||||
}
|
||||
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
urls.add(tablePath + "?filter=" + JsonUtils.toJson(filter));
|
||||
}
|
||||
|
||||
@ -208,6 +212,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return (null);
|
||||
}
|
||||
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return (tablePath + "?filter=" + JsonUtils.toJson(filter));
|
||||
}
|
||||
|
||||
@ -224,6 +229,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return (null);
|
||||
}
|
||||
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
}
|
||||
|
||||
@ -326,6 +332,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
}
|
||||
|
||||
String tablePath = QContext.getQInstance().getTablePath(tableName);
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return (tablePath + "/" + processName + "?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,202 @@
|
||||
/*
|
||||
* 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.dashboard.widgets;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.TableData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Generic widget that does an aggregate query, and presents its results
|
||||
** as a table, using group-by values as both row & column labels.
|
||||
*******************************************************************************/
|
||||
public class Aggregate2DTableWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(Aggregate2DTableWidgetRenderer.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
Map<String, Serializable> values = input.getWidgetMetaData().getDefaultValues();
|
||||
|
||||
String tableName = ValueUtils.getValueAsString(values.get("tableName"));
|
||||
String valueField = ValueUtils.getValueAsString(values.get("valueField"));
|
||||
String rowField = ValueUtils.getValueAsString(values.get("rowField"));
|
||||
String columnField = ValueUtils.getValueAsString(values.get("columnField"));
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
|
||||
AggregateInput aggregateInput = new AggregateInput();
|
||||
aggregateInput.setTableName(tableName);
|
||||
|
||||
// todo - allow input of "list of columns" (e.g., in case some miss sometimes, or as a version of filter)
|
||||
// todo - max rows, max cols?
|
||||
|
||||
// todo - from input map
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
aggregateInput.setFilter(filter);
|
||||
|
||||
Aggregate aggregate = new Aggregate(valueField, AggregateOperator.COUNT);
|
||||
aggregateInput.withAggregate(aggregate);
|
||||
|
||||
GroupBy rowGroupBy = new GroupBy(table.getField(rowField));
|
||||
GroupBy columnGroupBy = new GroupBy(table.getField(columnField));
|
||||
aggregateInput.withGroupBy(rowGroupBy);
|
||||
aggregateInput.withGroupBy(columnGroupBy);
|
||||
|
||||
String orderBys = ValueUtils.getValueAsString(values.get("orderBys"));
|
||||
if(StringUtils.hasContent(orderBys))
|
||||
{
|
||||
for(String orderBy : orderBys.split(","))
|
||||
{
|
||||
switch(orderBy)
|
||||
{
|
||||
case "row" -> filter.addOrderBy(new QFilterOrderByGroupBy(rowGroupBy));
|
||||
case "column" -> filter.addOrderBy(new QFilterOrderByGroupBy(columnGroupBy));
|
||||
case "value" -> filter.addOrderBy(new QFilterOrderByAggregate(aggregate));
|
||||
default -> LOG.warn("Unrecognized orderBy: " + orderBy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput);
|
||||
|
||||
Map<Serializable, Map<Serializable, Serializable>> data = new LinkedHashMap<>();
|
||||
Set<Serializable> columnsSet = new LinkedHashSet<>();
|
||||
|
||||
for(AggregateResult result : aggregateOutput.getResults())
|
||||
{
|
||||
Serializable column = result.getGroupByValue(columnGroupBy);
|
||||
Serializable row = result.getGroupByValue(rowGroupBy);
|
||||
Serializable value = result.getAggregateValue(aggregate);
|
||||
|
||||
Map<Serializable, Serializable> rowMap = data.computeIfAbsent(row, (k) -> new LinkedHashMap<>());
|
||||
rowMap.put(column, value);
|
||||
columnsSet.add(column);
|
||||
}
|
||||
|
||||
// todo - possible values from rows, cols
|
||||
|
||||
////////////////////////////////////
|
||||
// setup datastructures for table //
|
||||
////////////////////////////////////
|
||||
List<Map<String, Object>> tableRows = new ArrayList<>();
|
||||
List<TableData.Column> tableColumns = new ArrayList<>();
|
||||
tableColumns.add(new TableData.Column("default", table.getField(rowField).getLabel(), "_row", "2fr", "left"));
|
||||
|
||||
for(Serializable column : columnsSet)
|
||||
{
|
||||
tableColumns.add(new TableData.Column("default", String.valueOf(column) /* todo display value */, String.valueOf(column), "1fr", "right"));
|
||||
}
|
||||
|
||||
tableColumns.add(new TableData.Column("default", "Total", "_total", "1fr", "right"));
|
||||
|
||||
TableData tableData = new TableData(null, tableColumns, tableRows)
|
||||
.withRowsPerPage(100)
|
||||
.withFixedStickyLastRow(false)
|
||||
.withHidePaginationDropdown(true);
|
||||
|
||||
Map<Serializable, Integer> columnSums = new HashMap<>();
|
||||
int grandTotal = 0;
|
||||
for(Map.Entry<Serializable, Map<Serializable, Serializable>> rowEntry : data.entrySet())
|
||||
{
|
||||
Map<String, Object> rowMap = new HashMap<>();
|
||||
tableRows.add(rowMap);
|
||||
|
||||
rowMap.put("_row", rowEntry.getKey() /* todo display value */);
|
||||
int rowTotal = 0;
|
||||
for(Serializable column : columnsSet)
|
||||
{
|
||||
Serializable value = rowEntry.getValue().get(column);
|
||||
if(value == null)
|
||||
{
|
||||
value = 0; // todo?
|
||||
}
|
||||
|
||||
Integer valueAsInteger = Objects.requireNonNullElse(ValueUtils.getValueAsInteger(value), 0);
|
||||
rowTotal += valueAsInteger;
|
||||
columnSums.putIfAbsent(column, 0);
|
||||
columnSums.put(column, columnSums.get(column) + valueAsInteger);
|
||||
|
||||
rowMap.put(String.valueOf(column), value); // todo format commas?
|
||||
}
|
||||
|
||||
rowMap.put("_total", rowTotal);
|
||||
grandTotal += rowTotal;
|
||||
}
|
||||
|
||||
///////////////
|
||||
// total row //
|
||||
///////////////
|
||||
Map<String, Object> totalRowMap = new HashMap<>();
|
||||
tableRows.add(totalRowMap);
|
||||
|
||||
totalRowMap.put("_row", "Total");
|
||||
int rowTotal = 0;
|
||||
for(Serializable column : columnsSet)
|
||||
{
|
||||
Serializable value = columnSums.get(column);
|
||||
if(value == null)
|
||||
{
|
||||
value = 0; // todo?
|
||||
}
|
||||
|
||||
totalRowMap.put(String.valueOf(column), value); // todo format commas?
|
||||
}
|
||||
|
||||
totalRowMap.put("_total", grandTotal);
|
||||
|
||||
return (new RenderWidgetOutput(tableData));
|
||||
}
|
||||
|
||||
}
|
@ -137,7 +137,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
*******************************************************************************/
|
||||
public Builder withCanAddChildRecord(boolean b)
|
||||
{
|
||||
widgetMetaData.withDefaultValue("canAddChildRecord", true);
|
||||
widgetMetaData.withDefaultValue("canAddChildRecord", b);
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -151,6 +151,17 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
widgetMetaData.withDefaultValue("disabledFieldsForNewChildRecords", new HashSet<>(disabledFieldsForNewChildRecords));
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withManageAssociationName(String manageAssociationName)
|
||||
{
|
||||
widgetMetaData.withDefaultValue("manageAssociationName", manageAssociationName);
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -178,52 +189,60 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// fetch the record that we're getting children for. //
|
||||
// e.g., the left-side of the join, with the input id //
|
||||
////////////////////////////////////////////////////////
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(join.getLeftTable());
|
||||
getInput.setPrimaryKey(id);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
QRecord record = getOutput.getRecord();
|
||||
|
||||
if(record == null)
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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))
|
||||
{
|
||||
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
|
||||
}
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(join.getLeftTable());
|
||||
getInput.setPrimaryKey(id);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
primaryRecord = getOutput.getRecord();
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// set up the query - for the table on the right side of the join //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(record.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 queryOutput = new QueryAction().execute(queryInput);
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// set up the query - for the table on the right side of the join //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
filter = new QQueryFilter();
|
||||
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);
|
||||
|
||||
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(join.getRightTable());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
queryInput.setFilter(filter);
|
||||
queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
int 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();
|
||||
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());
|
||||
@ -239,10 +258,14 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
// new child records must have values from the join-ons //
|
||||
//////////////////////////////////////////////////////////
|
||||
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
if(primaryRecord != null)
|
||||
{
|
||||
defaultValuesForNewChildRecords.put(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
defaultValuesForNewChildRecords.put(joinOn.getRightField(), primaryRecord.getValue(joinOn.getLeftField()));
|
||||
}
|
||||
}
|
||||
|
||||
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
|
||||
|
||||
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
|
||||
@ -250,6 +273,22 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (new RenderWidgetOutput(widgetData));
|
||||
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
}
|
@ -34,15 +34,18 @@ import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithName;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
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.StringUtils;
|
||||
@ -333,9 +336,25 @@ public class PermissionsHelper
|
||||
QPermissionRules rules = getEffectivePermissionRules(tableMetaData, instance);
|
||||
String baseName = getEffectivePermissionBaseName(rules, tableMetaData.getName());
|
||||
|
||||
for(TablePermissionSubType permissionSubType : TablePermissionSubType.values())
|
||||
QBackendMetaData backend = instance.getBackend(tableMetaData.getBackendName());
|
||||
if(tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_INSERT))
|
||||
{
|
||||
addEffectiveAvailablePermission(rules, permissionSubType, rs, baseName, tableMetaData, "Table");
|
||||
addEffectiveAvailablePermission(rules, TablePermissionSubType.INSERT, rs, baseName, tableMetaData, "Table");
|
||||
}
|
||||
|
||||
if(tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_UPDATE))
|
||||
{
|
||||
addEffectiveAvailablePermission(rules, TablePermissionSubType.EDIT, rs, baseName, tableMetaData, "Table");
|
||||
}
|
||||
|
||||
if(tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_DELETE))
|
||||
{
|
||||
addEffectiveAvailablePermission(rules, TablePermissionSubType.DELETE, rs, baseName, tableMetaData, "Table");
|
||||
}
|
||||
|
||||
if(tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_QUERY) || tableMetaData.isCapabilityEnabled(backend, Capability.TABLE_GET))
|
||||
{
|
||||
addEffectiveAvailablePermission(rules, TablePermissionSubType.READ, rs, baseName, tableMetaData, "Table");
|
||||
}
|
||||
}
|
||||
|
||||
@ -369,7 +388,10 @@ public class PermissionsHelper
|
||||
{
|
||||
QPermissionRules rules = getEffectivePermissionRules(widgetMetaData, instance);
|
||||
String baseName = getEffectivePermissionBaseName(rules, widgetMetaData.getName());
|
||||
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, widgetMetaData, "Widget");
|
||||
if(!rules.getLevel().equals(PermissionLevel.NOT_PROTECTED))
|
||||
{
|
||||
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, widgetMetaData, "Widget");
|
||||
}
|
||||
}
|
||||
|
||||
return (rs);
|
||||
|
@ -26,8 +26,15 @@ import java.io.Serializable;
|
||||
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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -65,4 +72,46 @@ 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))));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -497,10 +497,10 @@ public class RunProcessAction
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if backend specifies that it uses variants, look for that data in the session and append to our basepull key //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(process.getSchedule() != null && process.getSchedule().getVariantBackend() != null)
|
||||
if(process.getSchedule() != null && process.getVariantBackend() != null)
|
||||
{
|
||||
QSession session = QContext.getQSession();
|
||||
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getSchedule().getVariantBackend());
|
||||
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getVariantBackend());
|
||||
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
|
||||
{
|
||||
LOG.info("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
|
||||
|
@ -31,6 +31,7 @@ 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;
|
||||
|
||||
@ -65,12 +66,12 @@ public class CsvExportStreamer implements ExportStreamerInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
|
||||
{
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
table = exportInput.getTable();
|
||||
outputStream = this.exportInput.getReportOutputStream();
|
||||
outputStream = this.exportInput.getReportDestination().getReportOutputStream();
|
||||
|
||||
writeTitleAndHeader();
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ 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;
|
||||
@ -138,7 +139,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.getReportFormat();
|
||||
ReportFormat reportFormat = exportInput.getReportDestination().getReportFormat();
|
||||
verifyCountUnderMax(exportInput, backendModule, reportFormat);
|
||||
|
||||
preExecuteRan = true;
|
||||
@ -232,6 +233,7 @@ 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 //
|
||||
@ -242,10 +244,19 @@ 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.getReportFormat();
|
||||
ReportFormat reportFormat = exportInput.getReportDestination().getReportFormat();
|
||||
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
|
||||
List<QFieldMetaData> fields = getFields(exportInput);
|
||||
reportStreamer.start(exportInput, fields, "Sheet 1");
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// 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));
|
||||
|
||||
//////////////////////////////////////////
|
||||
// run the query action as an async job //
|
||||
@ -334,7 +345,7 @@ public class ExportAction
|
||||
|
||||
try
|
||||
{
|
||||
exportInput.getReportOutputStream().close();
|
||||
exportInput.getReportDestination().getReportOutputStream().close();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
|
@ -26,8 +26,10 @@ 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -35,20 +37,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
*******************************************************************************/
|
||||
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 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.
|
||||
** Called once, before any sheets are actually being produced.
|
||||
*******************************************************************************/
|
||||
void finish() throws QReportingException;
|
||||
default void preRun(ReportDestination reportDestination, List<QReportView> views) throws QReportingException
|
||||
{
|
||||
// noop in base class
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -58,6 +54,20 @@ 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;
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -65,4 +75,11 @@ 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,6 +24,8 @@ 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;
|
||||
@ -32,18 +34,23 @@ 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;
|
||||
@ -51,6 +58,9 @@ 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;
|
||||
@ -68,11 +78,17 @@ 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -87,7 +103,7 @@ import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
|
||||
** - 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
|
||||
public class GenerateReportAction extends AbstractQActionFunction<ReportInput, ReportOutput>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(GenerateReportAction.class);
|
||||
|
||||
@ -101,50 +117,67 @@ public class GenerateReportAction
|
||||
// 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 void execute(ReportInput reportInput) throws QException
|
||||
public ReportOutput execute(ReportInput reportInput) throws QException
|
||||
{
|
||||
report = reportInput.getInstance().getReport(reportInput.getReportName());
|
||||
reportFormat = reportInput.getReportFormat();
|
||||
ReportOutput reportOutput = new ReportOutput();
|
||||
QReportMetaData report = getReportMetaData(reportInput);
|
||||
|
||||
this.views = report.getViews();
|
||||
this.dataSources = report.getDataSources();
|
||||
|
||||
ReportFormat reportFormat = reportInput.getReportDestination().getReportFormat();
|
||||
if(reportFormat == null)
|
||||
{
|
||||
throw new QException("Report format was not specified.");
|
||||
}
|
||||
reportStreamer = reportFormat.newReportStreamer();
|
||||
|
||||
if(reportInput.getOverrideExportStreamerSupplier() != null)
|
||||
{
|
||||
reportStreamer = reportInput.getOverrideExportStreamerSupplier().get();
|
||||
}
|
||||
else
|
||||
{
|
||||
reportStreamer = reportFormat.newReportStreamer();
|
||||
}
|
||||
|
||||
reportStreamer.preRun(reportInput.getReportDestination(), views);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach data source, do a query (possibly more than 1, if it goes to multiple table views) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QReportDataSource dataSource : report.getDataSources())
|
||||
for(QReportDataSource dataSource : dataSources)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// make a list of the views that use this data source for various purposes. //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
List<QReportView> dataSourceTableViews = report.getViews().stream()
|
||||
List<QReportView> dataSourceTableViews = views.stream()
|
||||
.filter(v -> v.getType().equals(ReportType.TABLE))
|
||||
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
|
||||
.toList();
|
||||
|
||||
List<QReportView> dataSourceSummaryViews = report.getViews().stream()
|
||||
List<QReportView> dataSourceSummaryViews = views.stream()
|
||||
.filter(v -> v.getType().equals(ReportType.SUMMARY))
|
||||
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
|
||||
.toList();
|
||||
|
||||
List<QReportView> dataSourceVariantViews = report.getViews().stream()
|
||||
List<QReportView> dataSourceVariantViews = views.stream()
|
||||
.filter(v -> v.getType().equals(ReportType.SUMMARY))
|
||||
.filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName()))
|
||||
.toList();
|
||||
@ -183,24 +216,50 @@ public class GenerateReportAction
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// start the table-view (e.g., open this tab in xlsx) and then run the query-loop //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
startTableView(reportInput, dataSource, dataSourceTableView);
|
||||
startTableView(reportInput, dataSource, dataSourceTableView, reportFormat);
|
||||
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.getReportOutputStream().close();
|
||||
reportInput.getReportDestination().getReportOutputStream().close();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error completing report", e));
|
||||
}
|
||||
|
||||
return (reportOutput);
|
||||
}
|
||||
|
||||
|
||||
@ -208,28 +267,48 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QException
|
||||
private QReportMetaData getReportMetaData(ReportInput reportInput) throws QException
|
||||
{
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
if(reportInput.getReportMetaData() != null)
|
||||
{
|
||||
return reportInput.getReportMetaData();
|
||||
}
|
||||
|
||||
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.setReportFormat(reportFormat);
|
||||
exportInput.setFilename(reportInput.getFilename());
|
||||
exportInput.setReportDestination(reportInput.getReportDestination());
|
||||
exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
|
||||
exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow());
|
||||
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
||||
|
||||
JoinsContext joinsContext = null;
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
if(dataSource != null)
|
||||
{
|
||||
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
|
||||
countDataSourceRecords(reportInput, dataSource, reportFormat);
|
||||
}
|
||||
}
|
||||
|
||||
List<QFieldMetaData> fields = new ArrayList<>();
|
||||
for(QReportField column : reportView.getColumns())
|
||||
for(QReportField column : CollectionUtils.nonNullList(reportView.getColumns()))
|
||||
{
|
||||
if(column.getIsVirtual())
|
||||
{
|
||||
@ -241,7 +320,7 @@ public class GenerateReportAction
|
||||
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.getName() + "]");
|
||||
throw new QReportingException("Could not find field named [" + effectiveFieldName + "] in dataSource [" + (dataSource == null ? null : dataSource.getName()) + "]");
|
||||
}
|
||||
|
||||
QFieldMetaData field = fieldAndTableNameOrAlias.field().clone();
|
||||
@ -255,7 +334,7 @@ public class GenerateReportAction
|
||||
}
|
||||
|
||||
reportStreamer.setDisplayFormats(getDisplayFormatMap(fields));
|
||||
reportStreamer.start(exportInput, fields, reportView.getLabel());
|
||||
reportStreamer.start(exportInput, fields, reportView.getLabel(), reportView);
|
||||
}
|
||||
|
||||
|
||||
@ -263,7 +342,36 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
|
||||
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
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check if this view has a transform step - if so, set it up now and run its pre-run //
|
||||
@ -273,7 +381,7 @@ public class GenerateReportAction
|
||||
RunBackendStepOutput transformStepOutput = null;
|
||||
if(tableView != null && tableView.getRecordTransformStep() != null)
|
||||
{
|
||||
transformStep = QCodeLoader.getBackendStep(AbstractTransformStep.class, tableView.getRecordTransformStep());
|
||||
transformStep = QCodeLoader.getAdHoc(AbstractTransformStep.class, tableView.getRecordTransformStep());
|
||||
|
||||
transformStepInput = new RunBackendStepInput();
|
||||
transformStepInput.setValues(reportInput.getInputValues());
|
||||
@ -290,6 +398,9 @@ public class GenerateReportAction
|
||||
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 //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
@ -306,6 +417,7 @@ public class GenerateReportAction
|
||||
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())));
|
||||
@ -349,6 +461,17 @@ public class GenerateReportAction
|
||||
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));
|
||||
});
|
||||
|
||||
@ -359,6 +482,8 @@ public class GenerateReportAction
|
||||
{
|
||||
transformStep.postRun(new BackendStepPostRunInput(transformStepInput), new BackendStepPostRunOutput(transformStepOutput));
|
||||
}
|
||||
|
||||
return consumedCount.get();
|
||||
}
|
||||
|
||||
|
||||
@ -366,11 +491,11 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext)
|
||||
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext) throws QException
|
||||
{
|
||||
Set<String> fieldsToTranslatePossibleValues = new HashSet<>();
|
||||
|
||||
for(QReportView view : report.getViews())
|
||||
for(QReportView view : views)
|
||||
{
|
||||
for(QReportField column : CollectionUtils.nonNullList(view.getColumns()))
|
||||
{
|
||||
@ -384,15 +509,16 @@ public class GenerateReportAction
|
||||
}
|
||||
}
|
||||
|
||||
for(String summaryField : CollectionUtils.nonNullList(view.getPivotFields()))
|
||||
for(String summaryFieldName : CollectionUtils.nonNullList(view.getSummaryFields()))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// all pivotFields that are possible value sources are implicitly translated //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
if(table.getField(summaryField).getPossibleValueSourceName() != null)
|
||||
QTableMetaData mainTable = QContext.getQInstance().getTable(dataSource.getSourceTable());
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(mainTable, summaryFieldName);
|
||||
if(fieldAndJoinTable.field().getPossibleValueSourceName() != null)
|
||||
{
|
||||
fieldsToTranslatePossibleValues.add(summaryField);
|
||||
fieldsToTranslatePossibleValues.add(summaryFieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -402,6 +528,32 @@ public class GenerateReportAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -494,26 +646,27 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> aggregatesMap)
|
||||
private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>>> aggregatesMap) throws QException
|
||||
{
|
||||
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 summaryField : view.getPivotFields())
|
||||
for(String summaryFieldName : view.getSummaryFields())
|
||||
{
|
||||
Serializable summaryValue = record.getValue(summaryField);
|
||||
if(table.getField(summaryField).getPossibleValueSourceName() != null)
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, summaryFieldName);
|
||||
Serializable summaryValue = record.getValue(summaryFieldName);
|
||||
if(fieldAndJoinTable.field().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(summaryField);
|
||||
summaryValue = record.getDisplayValue(summaryFieldName);
|
||||
}
|
||||
key.add(summaryField, summaryValue);
|
||||
key.add(summaryFieldName, summaryValue);
|
||||
|
||||
if(view.getIncludePivotSubTotals() && key.getKeys().size() < view.getPivotFields().size())
|
||||
if(view.getIncludeSummarySubTotals() && key.getKeys().size() < view.getSummaryFields().size())
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// be careful here, with these key objects, and their identity, being used as map keys //
|
||||
@ -532,9 +685,9 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates, SummaryKey key)
|
||||
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?, ?>>> viewAggregates, SummaryKey key) throws QException
|
||||
{
|
||||
Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
|
||||
Map<String, AggregatesInterface<?, ?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
|
||||
addRecordToAggregatesMap(table, record, keyAggregates);
|
||||
}
|
||||
|
||||
@ -543,23 +696,58 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?>> aggregatesMap)
|
||||
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?, ?>> aggregatesMap) throws QException
|
||||
{
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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())
|
||||
{
|
||||
if(field.getType().equals(QFieldType.INTEGER))
|
||||
FieldAndJoinTable fieldAndJoinTable = getFieldAndJoinTable(table, fieldName);
|
||||
QFieldMetaData field = fieldAndJoinTable.field();
|
||||
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
AggregatesInterface<Integer> fieldAggregates = (AggregatesInterface<Integer>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
|
||||
fieldAggregates.add(record.getValueInteger(field.getName()));
|
||||
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));
|
||||
}
|
||||
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));
|
||||
}
|
||||
else if(field.getType().equals(QFieldType.DECIMAL))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
AggregatesInterface<BigDecimal> fieldAggregates = (AggregatesInterface<BigDecimal>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
|
||||
fieldAggregates.add(record.getValueBigDecimal(field.getName()));
|
||||
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));
|
||||
}
|
||||
// todo - more types (dates, at least?)
|
||||
}
|
||||
}
|
||||
|
||||
@ -568,24 +756,22 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void outputSummaries(ReportInput reportInput) throws QReportingException, QFormulaException
|
||||
private void outputSummaries(ReportInput reportInput) throws QException
|
||||
{
|
||||
List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
|
||||
List<QReportView> reportViews = views.stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
|
||||
for(QReportView view : reportViews)
|
||||
{
|
||||
QReportDataSource dataSource = report.getDataSource(view.getDataSourceName());
|
||||
QReportDataSource dataSource = getDataSource(view.getDataSourceName());
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
|
||||
|
||||
ExportInput exportInput = new ExportInput();
|
||||
exportInput.setReportFormat(reportFormat);
|
||||
exportInput.setFilename(reportInput.getFilename());
|
||||
exportInput.setReportDestination(reportInput.getReportDestination());
|
||||
exportInput.setTitleRow(summaryOutput.titleRow);
|
||||
exportInput.setIncludeHeaderRow(view.getIncludeHeaderRow());
|
||||
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
||||
|
||||
reportStreamer.setDisplayFormats(getDisplayFormatMap(view));
|
||||
reportStreamer.start(exportInput, getFields(table, view), view.getLabel());
|
||||
reportStreamer.start(exportInput, getFields(table, view), view.getLabel(), view);
|
||||
|
||||
reportStreamer.addRecords(summaryOutput.summaryRows); // todo - what if this set is huge?
|
||||
|
||||
@ -598,6 +784,24 @@ public class GenerateReportAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QReportDataSource getDataSource(String dataSourceName)
|
||||
{
|
||||
for(QReportDataSource dataSource : CollectionUtils.nonNullList(dataSources))
|
||||
{
|
||||
if(dataSource.getName().equals(dataSourceName))
|
||||
{
|
||||
return (dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -625,13 +829,13 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QFieldMetaData> getFields(QTableMetaData table, QReportView view)
|
||||
private List<QFieldMetaData> getFields(QTableMetaData table, QReportView view) throws QException
|
||||
{
|
||||
List<QFieldMetaData> fields = new ArrayList<>();
|
||||
for(String pivotField : view.getPivotFields())
|
||||
for(String summaryFieldName : view.getSummaryFields())
|
||||
{
|
||||
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
|
||||
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
|
||||
}
|
||||
for(QReportField column : view.getColumns())
|
||||
{
|
||||
@ -661,11 +865,11 @@ public class GenerateReportAction
|
||||
// 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);
|
||||
|
||||
@ -674,9 +878,9 @@ public class GenerateReportAction
|
||||
|
||||
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);
|
||||
}
|
||||
@ -695,7 +899,7 @@ public class GenerateReportAction
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// for summary subtotals, add the text "Total" to the last field in this key //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
if(summaryKey.getKeys().size() < view.getPivotFields().size())
|
||||
if(summaryKey.getKeys().size() < view.getSummaryFields().size())
|
||||
{
|
||||
String fieldName = summaryKey.getKeys().get(summaryKey.getKeys().size() - 1).getA();
|
||||
summaryRow.setValue(fieldName, summaryRow.getValueString(fieldName) + " Total");
|
||||
@ -733,11 +937,11 @@ public class GenerateReportAction
|
||||
{
|
||||
totalRow = new QRecord();
|
||||
|
||||
for(String pivotField : view.getPivotFields())
|
||||
for(String summaryField : view.getSummaryFields())
|
||||
{
|
||||
if(totalRow.getValues().isEmpty())
|
||||
{
|
||||
totalRow.setValue(pivotField, "Totals");
|
||||
totalRow.setValue(summaryField, "Totals");
|
||||
}
|
||||
}
|
||||
|
||||
@ -857,18 +1061,24 @@ public class GenerateReportAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
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;
|
||||
}
|
||||
@ -882,4 +1092,10 @@ public class GenerateReportAction
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private record FieldAndJoinTable(QFieldMetaData field, QTableMetaData joinTable) {}
|
||||
}
|
||||
|
@ -26,17 +26,23 @@ 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.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -46,13 +52,23 @@ 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 needComma = false;
|
||||
private boolean prettyPrint = true;
|
||||
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<>();
|
||||
|
||||
|
||||
|
||||
@ -69,21 +85,124 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
|
||||
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
|
||||
{
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
table = exportInput.getTable();
|
||||
outputStream = this.exportInput.getReportOutputStream();
|
||||
|
||||
needCommaBeforeRecord = false;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -111,7 +230,7 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
try
|
||||
{
|
||||
if(needComma)
|
||||
if(needCommaBeforeRecord)
|
||||
{
|
||||
outputStream.write(',');
|
||||
}
|
||||
@ -119,21 +238,25 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
Map<String, Serializable> mapForJson = new LinkedHashMap<>();
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
String labelForJson = StringUtils.lcFirst(field.getLabel().replace(" ", ""));
|
||||
mapForJson.put(labelForJson, qRecord.getValue(field.getName()));
|
||||
mapForJson.put(getLabelForJson(field), 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?
|
||||
needComma = true;
|
||||
needCommaBeforeRecord = true;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -143,6 +266,73 @@ 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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -162,11 +352,34 @@ public class JsonExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
try
|
||||
{
|
||||
if(prettyPrint)
|
||||
{
|
||||
outputStream.write('\n');
|
||||
}
|
||||
//////////////////////////////////////////////
|
||||
// close the array of entries for this view //
|
||||
//////////////////////////////////////////////
|
||||
newlineIfPretty(outputStream);
|
||||
|
||||
decreaseIndent();
|
||||
indentIfPretty(outputStream);
|
||||
outputStream.write(']');
|
||||
newlineIfPretty(outputStream);
|
||||
|
||||
if(multipleViews)
|
||||
{
|
||||
////////////////////////////////////////////
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
@ -174,4 +387,30 @@ 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,6 +31,7 @@ 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -87,7 +88,7 @@ public class ListOfMapsExportStreamer implements ExportStreamerInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
|
||||
{
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
|
@ -44,7 +44,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 ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(1_000);
|
||||
private int capacity = 1_000;
|
||||
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(capacity);
|
||||
|
||||
private boolean isTerminated = false;
|
||||
|
||||
@ -72,6 +73,7 @@ public class RecordPipe
|
||||
*******************************************************************************/
|
||||
public RecordPipe(Integer overrideCapacity)
|
||||
{
|
||||
this.capacity = overrideCapacity;
|
||||
queue = new ArrayBlockingQueue<>(overrideCapacity);
|
||||
}
|
||||
|
||||
@ -213,4 +215,14 @@ public class RecordPipe
|
||||
this.postRecordActions = postRecordActions;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for capacity
|
||||
**
|
||||
*******************************************************************************/
|
||||
public int getCapacity()
|
||||
{
|
||||
return capacity;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
|
||||
|
||||
|
||||
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 BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
|
||||
public class BoldHeaderAndFooterFastExcelStyler implements FastExcelStylerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
@ -60,6 +60,9 @@ public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void styleTotalsRow(StyleSetter totalsRowStyle)
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting;
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
|
||||
|
||||
|
||||
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.excelformatting.ExcelStylerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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,7 +43,9 @@ 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;
|
||||
@ -52,19 +54,19 @@ import org.dhatim.fastexcel.Worksheet;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Excel export format implementation
|
||||
** Excel export format implementation - built using fastexcel library
|
||||
*******************************************************************************/
|
||||
public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
public class ExcelFastexcelExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ExcelExportStreamer.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(ExcelFastexcelExportStreamer.class);
|
||||
|
||||
private ExportInput exportInput;
|
||||
private QTableMetaData table;
|
||||
private List<QFieldMetaData> fields;
|
||||
private OutputStream outputStream;
|
||||
|
||||
private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
|
||||
private Map<String, String> excelCellFormats;
|
||||
private FastExcelStylerInterface fastExcelStylerInterface = new PlainFastExcelStyler();
|
||||
private Map<String, String> excelCellFormats;
|
||||
|
||||
private Workbook workbook;
|
||||
private Worksheet worksheet;
|
||||
@ -76,7 +78,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ExcelExportStreamer()
|
||||
public ExcelFastexcelExportStreamer()
|
||||
{
|
||||
}
|
||||
|
||||
@ -105,14 +107,14 @@ public class ExcelExportStreamer 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) throws QReportingException
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label, QReportView view) throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
table = exportInput.getTable();
|
||||
outputStream = this.exportInput.getReportOutputStream();
|
||||
outputStream = this.exportInput.getReportDestination().getReportOutputStream();
|
||||
this.row = 0;
|
||||
this.sheetCount++;
|
||||
|
||||
@ -121,7 +123,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(workbook == null)
|
||||
{
|
||||
String appName = "QQQ";
|
||||
String appName = ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getBranding().getAppName(), "QQQ");
|
||||
QInstance instance = exportInput.getInstance();
|
||||
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
|
||||
{
|
||||
@ -167,7 +169,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
worksheet.range(row, 0, row, fields.size() - 1).merge();
|
||||
|
||||
StyleSetter titleStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
|
||||
excelStylerInterface.styleTitleRow(titleStyle);
|
||||
fastExcelStylerInterface.styleTitleRow(titleStyle);
|
||||
titleStyle.set();
|
||||
|
||||
row++;
|
||||
@ -187,7 +189,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
}
|
||||
|
||||
StyleSetter headerStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
|
||||
excelStylerInterface.styleHeaderRow(headerStyle);
|
||||
fastExcelStylerInterface.styleHeaderRow(headerStyle);
|
||||
headerStyle.set();
|
||||
|
||||
row++;
|
||||
@ -315,7 +317,7 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
writeRecord(record);
|
||||
|
||||
StyleSetter totalsRowStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
|
||||
excelStylerInterface.styleTotalsRow(totalsRowStyle);
|
||||
fastExcelStylerInterface.styleTotalsRow(totalsRowStyle);
|
||||
totalsRowStyle.set();
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excelformatting;
|
||||
package com.kingsrook.qqq.backend.core.actions.reporting.excel.fastexcel;
|
||||
|
||||
|
||||
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 ExcelStylerInterface
|
||||
public interface FastExcelStylerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,800 @@
|
||||
/*
|
||||
* 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.
|
||||
*******************************************************************************/
|
||||
public class ExcelPoiBasedStreamingExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ExcelPoiBasedStreamingExportStreamer.class);
|
||||
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 List<QReportView> views;
|
||||
private ExportInput exportInput;
|
||||
private List<QFieldMetaData> fields;
|
||||
private OutputStream outputStream;
|
||||
private ZipOutputStream zipOutputStream;
|
||||
|
||||
private PoiExcelStylerInterface poiExcelStylerInterface = getStylerInterface();
|
||||
private Map<String, String> excelCellFormats;
|
||||
|
||||
private int rowNo = 0;
|
||||
private int sheetIndex = 1;
|
||||
|
||||
private Map<String, String> pivotViewToCacheDefinitionReferenceMap = new HashMap<>();
|
||||
private Map<String, XSSFCellStyle> styles = new HashMap<>();
|
||||
|
||||
private Writer activeSheetWriter = null;
|
||||
private StreamedPoiSheetWriter 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. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
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++);
|
||||
// todo ... not like this
|
||||
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());
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - some bug where, if use a group-by field here, then ... it doesn't get used for the grouping. //
|
||||
// g-sheets does let me do this, so, maybe, download their file and see how it's different? //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
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 StreamedPoiSheetWriter(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 - formats...
|
||||
// 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)
|
||||
{
|
||||
// todo - what would be a better zone to use here?
|
||||
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();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - leave the zipOutputStream open. It is a wrapper around the OutputStream we were given by the caller, //
|
||||
// so it is their responsibility to close that stream (which implicitly closes the zip, it appears) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
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 labelO //
|
||||
/////////////////////////////////////////////////////////
|
||||
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.7113> 95767702E12" refreshOnLoad="true" r:id="rId1">
|
||||
<cacheSource type="worksheet">
|
||||
<worksheetSource sheet="%s" ref="A1:%s%d"/>
|
||||
</cacheSource>
|
||||
<cacheFields count="%d">
|
||||
%s
|
||||
</cacheFields>
|
||||
</pivotCacheDefinition>
|
||||
""",
|
||||
StreamedPoiSheetWriter.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());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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 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 StreamedPoiSheetWriter
|
||||
{
|
||||
private final Writer writer;
|
||||
private int rowNo;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public StreamedPoiSheetWriter(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)
|
||||
{
|
||||
if(value.indexOf('&') > -1 || value.indexOf('<') > -1 || value.indexOf('>') > -1 || value.indexOf('\'') > -1 || value.indexOf('"') > -1)
|
||||
{
|
||||
value = value.replace("&", "&");
|
||||
value = value.replace("<", "<");
|
||||
value = value.replace(">", ">");
|
||||
value = value.replace("'", "'");
|
||||
value = value.replace("\"", """);
|
||||
}
|
||||
}
|
||||
|
||||
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>");
|
||||
}
|
||||
|
||||
// todo? public void createCell(int columnIndex, Calendar value, int styleIndex) throws IOException
|
||||
// todo? {
|
||||
// todo? createCell(columnIndex, DateUtil.getExcelDate(value, false), styleIndex);
|
||||
// todo? }
|
||||
}
|
@ -35,9 +35,8 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostDeleteCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
|
||||
@ -250,7 +249,7 @@ public class DeleteAction
|
||||
//////////////////////////////////////////////////////////////
|
||||
// finally, run the post-delete customizer, if there is one //
|
||||
//////////////////////////////////////////////////////////////
|
||||
Optional<AbstractPostDeleteCustomizer> postDeleteCustomizer = QCodeLoader.getTableCustomizer(AbstractPostDeleteCustomizer.class, table, TableCustomizers.POST_DELETE_RECORD.getRole());
|
||||
Optional<TableCustomizerInterface> postDeleteCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_DELETE_RECORD.getRole());
|
||||
if(postDeleteCustomizer.isPresent() && oldRecordList.isPresent())
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@ -260,8 +259,7 @@ public class DeleteAction
|
||||
|
||||
try
|
||||
{
|
||||
postDeleteCustomizer.get().setDeleteInput(deleteInput);
|
||||
List<QRecord> postCustomizerResult = postDeleteCustomizer.get().apply(recordsForCustomizer);
|
||||
List<QRecord> postCustomizerResult = postDeleteCustomizer.get().postDelete(deleteInput, recordsForCustomizer);
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// check if any records got errors in the customizer //
|
||||
@ -327,13 +325,11 @@ public class DeleteAction
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// after all validations, run the pre-delete customizer, if there is one //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
Optional<AbstractPreDeleteCustomizer> preDeleteCustomizer = QCodeLoader.getTableCustomizer(AbstractPreDeleteCustomizer.class, table, TableCustomizers.PRE_DELETE_RECORD.getRole());
|
||||
Optional<TableCustomizerInterface> preDeleteCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_DELETE_RECORD.getRole());
|
||||
List<QRecord> customizerResult = oldRecordList.get();
|
||||
if(preDeleteCustomizer.isPresent())
|
||||
{
|
||||
preDeleteCustomizer.get().setDeleteInput(deleteInput);
|
||||
preDeleteCustomizer.get().setIsPreview(isPreview);
|
||||
customizerResult = preDeleteCustomizer.get().apply(oldRecordList.get());
|
||||
customizerResult = preDeleteCustomizer.get().preDelete(deleteInput, oldRecordList.get(), isPreview);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
|
@ -27,8 +27,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.GetActionCacheHelper;
|
||||
@ -49,6 +49,7 @@ 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.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -57,7 +58,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
*******************************************************************************/
|
||||
public class GetAction
|
||||
{
|
||||
private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer;
|
||||
private Optional<TableCustomizerInterface> postGetRecordCustomizer;
|
||||
|
||||
private GetInput getInput;
|
||||
private QPossibleValueTranslator qPossibleValueTranslator;
|
||||
@ -87,7 +88,7 @@ public class GetAction
|
||||
throw (new QException("Requested to Get a record from an unrecognized table: " + getInput.getTableName()));
|
||||
}
|
||||
|
||||
postGetRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
postGetRecordCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
this.getInput = getInput;
|
||||
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
@ -107,9 +108,11 @@ public class GetAction
|
||||
}
|
||||
|
||||
GetOutput getOutput;
|
||||
boolean usingDefaultGetInterface = false;
|
||||
if(getInterface == null)
|
||||
{
|
||||
getInterface = new DefaultGetInterface();
|
||||
usingDefaultGetInterface = true;
|
||||
}
|
||||
|
||||
getInterface.validateInput(getInput);
|
||||
@ -123,10 +126,11 @@ public class GetAction
|
||||
new GetActionCacheHelper().handleCaching(getInput, getOutput);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// if the record is found, perform post-actions on it //
|
||||
////////////////////////////////////////////////////////
|
||||
if(getOutput.getRecord() != null)
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the record is found, perform post-actions on it //
|
||||
// unless the defaultGetInterface was used - as it just does a query, and the query will do the post-actions. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(getOutput.getRecord() != null && !usingDefaultGetInterface)
|
||||
{
|
||||
getOutput.setRecord(postRecordActions(getOutput.getRecord()));
|
||||
}
|
||||
@ -202,7 +206,7 @@ public class GetAction
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
|
||||
throw (new QException("Unable to get " + ObjectUtils.tryElse(() -> queryInput.getTable().getLabel(), queryInput.getTableName()) + ". Missing required input."));
|
||||
}
|
||||
|
||||
queryInput.setFilter(filter);
|
||||
@ -216,12 +220,12 @@ public class GetAction
|
||||
** Run the necessary actions on a record. This may include setting display values,
|
||||
** translating possible values, and running post-record customizations.
|
||||
*******************************************************************************/
|
||||
public QRecord postRecordActions(QRecord record)
|
||||
public QRecord postRecordActions(QRecord record) throws QException
|
||||
{
|
||||
QRecord returnRecord = record;
|
||||
if(this.postGetRecordCustomizer.isPresent())
|
||||
{
|
||||
returnRecord = postGetRecordCustomizer.get().apply(List.of(record)).get(0);
|
||||
returnRecord = postGetRecordCustomizer.get().postQuery(getInput, List.of(record)).get(0);
|
||||
}
|
||||
|
||||
if(getInput.getShouldTranslatePossibleValues())
|
||||
|
@ -28,6 +28,7 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@ -37,9 +38,9 @@ import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.InsertInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
|
||||
@ -168,13 +169,12 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
//////////////////////////////////////////////////////////////
|
||||
// finally, run the post-insert customizer, if there is one //
|
||||
//////////////////////////////////////////////////////////////
|
||||
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
|
||||
Optional<TableCustomizerInterface> postInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_INSERT_RECORD.getRole());
|
||||
if(postInsertCustomizer.isPresent())
|
||||
{
|
||||
try
|
||||
{
|
||||
postInsertCustomizer.get().setInsertInput(insertInput);
|
||||
insertOutput.setRecords(postInsertCustomizer.get().apply(insertOutput.getRecords()));
|
||||
insertOutput.setRecords(postInsertCustomizer.get().postInsert(insertInput, insertOutput.getRecords()));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -232,31 +232,29 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
// load the pre-insert customizer and set it up, if there is one //
|
||||
// then we'll run it based on its WhenToRun value //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
Optional<AbstractPreInsertCustomizer> preInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPreInsertCustomizer.class, table, TableCustomizers.PRE_INSERT_RECORD.getRole());
|
||||
Optional<TableCustomizerInterface> preInsertCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_INSERT_RECORD.getRole());
|
||||
if(preInsertCustomizer.isPresent())
|
||||
{
|
||||
preInsertCustomizer.get().setInsertInput(insertInput);
|
||||
preInsertCustomizer.get().setIsPreview(isPreview);
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_ALL_VALIDATIONS);
|
||||
}
|
||||
|
||||
setDefaultValuesInRecords(table, insertInput.getRecords());
|
||||
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, insertInput.getInstance(), table, insertInput.getRecords());
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, insertInput.getInstance(), table, insertInput.getRecords(), null);
|
||||
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS);
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_UNIQUE_KEY_CHECKS);
|
||||
setErrorsIfUniqueKeyErrors(insertInput, table);
|
||||
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_REQUIRED_FIELD_CHECKS);
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_REQUIRED_FIELD_CHECKS);
|
||||
if(insertInput.getInputSource().shouldValidateRequiredFields())
|
||||
{
|
||||
validateRequiredFields(insertInput);
|
||||
}
|
||||
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_SECURITY_CHECKS);
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.BEFORE_SECURITY_CHECKS);
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
|
||||
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS);
|
||||
runPreInsertCustomizerIfItIsTime(insertInput, isPreview, preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun.AFTER_ALL_VALIDATIONS);
|
||||
}
|
||||
|
||||
|
||||
@ -290,13 +288,13 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void runPreInsertCustomizerIfItIsTime(InsertInput insertInput, Optional<AbstractPreInsertCustomizer> preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun whenToRun) throws QException
|
||||
private void runPreInsertCustomizerIfItIsTime(InsertInput insertInput, boolean isPreview, Optional<TableCustomizerInterface> preInsertCustomizer, AbstractPreInsertCustomizer.WhenToRun whenToRun) throws QException
|
||||
{
|
||||
if(preInsertCustomizer.isPresent())
|
||||
{
|
||||
if(whenToRun.equals(preInsertCustomizer.get().getWhenToRun()))
|
||||
if(whenToRun.equals(preInsertCustomizer.get().whenToRunPreInsert(insertInput, isPreview)))
|
||||
{
|
||||
insertInput.setRecords(preInsertCustomizer.get().apply(insertInput.getRecords()));
|
||||
insertInput.setRecords(preInsertCustomizer.get().preInsert(insertInput, insertInput.getRecords(), isPreview));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -321,7 +319,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
{
|
||||
if(record.getValue(requiredField.getName()) == null || (requiredField.getType().isStringLike() && record.getValueString(requiredField.getName()).trim().equals("")))
|
||||
{
|
||||
record.addError(new BadInputStatusMessage("Missing value in required field: " + requiredField.getLabel()));
|
||||
record.addError(new BadInputStatusMessage("Missing value in required field: " + Objects.requireNonNullElse(requiredField.getLabel(), requiredField.getName())));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -455,7 +453,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
private QBackendModuleInterface getBackendModuleInterface(QBackendMetaData backend) throws QException
|
||||
{
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
|
||||
return (qModule);
|
||||
}
|
||||
|
||||
|
@ -31,8 +31,8 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
|
||||
@ -73,7 +73,7 @@ public class QueryAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QueryAction.class);
|
||||
|
||||
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
|
||||
private Optional<TableCustomizerInterface> postQueryRecordCustomizer;
|
||||
|
||||
private QueryInput queryInput;
|
||||
private QueryInterface queryInterface;
|
||||
@ -100,7 +100,7 @@ public class QueryAction
|
||||
}
|
||||
|
||||
QBackendMetaData backend = queryInput.getBackend();
|
||||
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
this.queryInput = queryInput;
|
||||
|
||||
if(queryInput.getRecordPipe() != null)
|
||||
@ -264,7 +264,7 @@ public class QueryAction
|
||||
{
|
||||
if(this.postQueryRecordCustomizer.isPresent())
|
||||
{
|
||||
records = postQueryRecordCustomizer.get().apply(records);
|
||||
records = postQueryRecordCustomizer.get().postQuery(queryInput, records);
|
||||
}
|
||||
|
||||
if(queryInput.getShouldTranslatePossibleValues())
|
||||
|
@ -136,19 +136,22 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
output.setUpdateOutput(updateOutput);
|
||||
|
||||
QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep));
|
||||
if(input.getFilter() != null)
|
||||
if(input.getPerformDeletes())
|
||||
{
|
||||
deleteFilter.addSubFilter(input.getFilter());
|
||||
}
|
||||
QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep));
|
||||
if(input.getFilter() != null)
|
||||
{
|
||||
deleteFilter.addSubFilter(input.getFilter());
|
||||
}
|
||||
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(table.getName());
|
||||
deleteInput.setQueryFilter(deleteFilter);
|
||||
deleteInput.setTransaction(transaction);
|
||||
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
|
||||
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
|
||||
output.setDeleteOutput(deleteOutput);
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(table.getName());
|
||||
deleteInput.setQueryFilter(deleteFilter);
|
||||
deleteInput.setTransaction(transaction);
|
||||
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
|
||||
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
|
||||
output.setDeleteOutput(deleteOutput);
|
||||
}
|
||||
|
||||
if(weOwnTheTransaction)
|
||||
{
|
||||
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 query 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);
|
||||
}
|
||||
}
|
@ -34,9 +34,8 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostUpdateCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreUpdateCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
|
||||
@ -57,6 +56,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
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.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
@ -72,6 +73,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -189,14 +191,12 @@ public class UpdateAction
|
||||
//////////////////////////////////////////////////////////////
|
||||
// finally, run the post-update customizer, if there is one //
|
||||
//////////////////////////////////////////////////////////////
|
||||
Optional<AbstractPostUpdateCustomizer> postUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPostUpdateCustomizer.class, table, TableCustomizers.POST_UPDATE_RECORD.getRole());
|
||||
Optional<TableCustomizerInterface> postUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_UPDATE_RECORD.getRole());
|
||||
if(postUpdateCustomizer.isPresent())
|
||||
{
|
||||
try
|
||||
{
|
||||
postUpdateCustomizer.get().setUpdateInput(updateInput);
|
||||
oldRecordList.ifPresent(l -> postUpdateCustomizer.get().setOldRecordList(l));
|
||||
updateOutput.setRecords(postUpdateCustomizer.get().apply(updateOutput.getRecords()));
|
||||
updateOutput.setRecords(postUpdateCustomizer.get().postUpdate(updateInput, updateOutput.getRecords(), oldRecordList));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -244,7 +244,13 @@ public class UpdateAction
|
||||
/////////////////////////////
|
||||
// run standard validators //
|
||||
/////////////////////////////
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, updateInput.getInstance(), table, updateInput.getRecords());
|
||||
Set<FieldBehavior<?>> behaviorsToOmit = null;
|
||||
if(BooleanUtils.isTrue(updateInput.getOmitModifyDateUpdate()))
|
||||
{
|
||||
behaviorsToOmit = Set.of(DynamicDefaultValueBehavior.MODIFY_DATE);
|
||||
}
|
||||
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.UPDATE, updateInput.getInstance(), table, updateInput.getRecords(), behaviorsToOmit);
|
||||
validatePrimaryKeysAreGiven(updateInput);
|
||||
|
||||
if(oldRecordList.isPresent())
|
||||
@ -264,13 +270,10 @@ public class UpdateAction
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// after all validations, run the pre-update customizer, if there is one //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
Optional<AbstractPreUpdateCustomizer> preUpdateCustomizer = QCodeLoader.getTableCustomizer(AbstractPreUpdateCustomizer.class, table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
|
||||
Optional<TableCustomizerInterface> preUpdateCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.PRE_UPDATE_RECORD.getRole());
|
||||
if(preUpdateCustomizer.isPresent())
|
||||
{
|
||||
preUpdateCustomizer.get().setUpdateInput(updateInput);
|
||||
preUpdateCustomizer.get().setIsPreview(isPreview);
|
||||
oldRecordList.ifPresent(l -> preUpdateCustomizer.get().setOldRecordList(l));
|
||||
updateInput.setRecords(preUpdateCustomizer.get().apply(updateInput.getRecords()));
|
||||
updateInput.setRecords(preUpdateCustomizer.get().preUpdate(updateInput, updateInput.getRecords(), isPreview, oldRecordList));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ 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.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;
|
||||
@ -230,7 +231,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
{
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
|
||||
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."));
|
||||
}
|
||||
@ -298,7 +299,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// handle null values - error if the NullValueBehavior is DENY //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
|
||||
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
|
||||
|
@ -269,6 +269,10 @@ public class QPossibleValueTranslator
|
||||
{
|
||||
value = ValueUtils.getValueAsInteger(value);
|
||||
}
|
||||
if(field.getType().equals(QFieldType.LONG) && !(value instanceof Long))
|
||||
{
|
||||
value = ValueUtils.getValueAsLong(value);
|
||||
}
|
||||
}
|
||||
catch(QValueException e)
|
||||
{
|
||||
@ -366,6 +370,14 @@ public class QPossibleValueTranslator
|
||||
*******************************************************************************/
|
||||
private String translatePossibleValueCustom(Serializable value, QPossibleValueSource possibleValueSource)
|
||||
{
|
||||
/////////////////////////////////
|
||||
// null input gets null output //
|
||||
/////////////////////////////////
|
||||
if(value == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
|
||||
|
@ -490,6 +490,13 @@ public class QValueFormatter
|
||||
{
|
||||
adornmentValues = fileDownloadAdornment.get().getValues();
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// don't change blobs unless they are file-downloads //
|
||||
///////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
String fileNameField = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FIELD));
|
||||
String fileNameFormat = ValueUtils.getValueAsString(adornmentValues.get(AdornmentType.FileDownloadValues.FILE_NAME_FORMAT));
|
||||
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
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;
|
||||
@ -32,7 +33,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class to apply value behaviors to records.
|
||||
** Utility class to apply value behaviors to records.
|
||||
*******************************************************************************/
|
||||
public class ValueBehaviorApplier
|
||||
{
|
||||
@ -51,7 +52,7 @@ public class ValueBehaviorApplier
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void applyFieldBehaviors(Action action, QInstance instance, QTableMetaData table, List<QRecord> recordList)
|
||||
public static void applyFieldBehaviors(Action action, QInstance instance, QTableMetaData table, List<QRecord> recordList, Set<FieldBehavior<?>> behaviorsToOmit)
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(recordList))
|
||||
{
|
||||
@ -62,7 +63,7 @@ public class ValueBehaviorApplier
|
||||
{
|
||||
for(FieldBehavior<?> fieldBehavior : CollectionUtils.nonNullCollection(field.getBehaviors()))
|
||||
{
|
||||
fieldBehavior.apply(action, recordList, instance, table, field);
|
||||
fieldBehavior.apply(action, recordList, instance, table, field, behaviorsToOmit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,5 +34,11 @@ import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
*******************************************************************************/
|
||||
public record CapturedContext(QInstance qInstance, QSession qSession, QBackendTransaction qBackendTransaction, Stack<AbstractActionInput> actionStack)
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Simpler constructor
|
||||
*******************************************************************************/
|
||||
public CapturedContext(QInstance qInstance, QSession qSession)
|
||||
{
|
||||
this(qInstance, qSession, null, null);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,9 @@
|
||||
package com.kingsrook.qqq.backend.core.context;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Stack;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
@ -31,6 +34,8 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeVoidVoidMethod;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -47,6 +52,8 @@ public class QContext
|
||||
private static ThreadLocal<QBackendTransaction> qBackendTransactionThreadLocal = new ThreadLocal<>();
|
||||
private static ThreadLocal<Stack<AbstractActionInput>> actionStackThreadLocal = new ThreadLocal<>();
|
||||
|
||||
private static ThreadLocal<Map<String, Serializable>> objectsThreadLocal = new ThreadLocal<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -100,6 +107,25 @@ public class QContext
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T extends Throwable> void withTemporaryContext(CapturedContext context, UnsafeVoidVoidMethod<T> method) throws T
|
||||
{
|
||||
CapturedContext originalContext = QContext.capture();
|
||||
try
|
||||
{
|
||||
QContext.init(context);
|
||||
method.run();
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.init(originalContext);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Init a new thread with the context captured from a different thread. e.g.,
|
||||
** when starting some async task.
|
||||
@ -132,6 +158,7 @@ public class QContext
|
||||
qSessionThreadLocal.remove();
|
||||
qBackendTransactionThreadLocal.remove();
|
||||
actionStackThreadLocal.remove();
|
||||
objectsThreadLocal.remove();
|
||||
}
|
||||
|
||||
|
||||
@ -259,4 +286,94 @@ public class QContext
|
||||
|
||||
return (Optional.of(actionStackThreadLocal.get().get(0)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get one named object from the Context for the current thread. may return null.
|
||||
*******************************************************************************/
|
||||
public static Serializable getObject(String key)
|
||||
{
|
||||
if(objectsThreadLocal.get() == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return objectsThreadLocal.get().get(key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get one named object from the Context for the current thread, cast to the
|
||||
** specified type if possible. if not found, or wrong type, empty is returned.
|
||||
*******************************************************************************/
|
||||
public static <T extends Serializable> Optional<T> getObject(String key, Class<T> type)
|
||||
{
|
||||
Serializable object = getObject(key);
|
||||
|
||||
if(type.isInstance(object))
|
||||
{
|
||||
return Optional.of(type.cast(object));
|
||||
}
|
||||
else if(object == null)
|
||||
{
|
||||
return Optional.empty();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unexpected type of object found in session under key [" + key + "]",
|
||||
logPair("expectedType", type.getName()),
|
||||
logPair("actualType", object.getClass().getName())
|
||||
);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** put a named object into the Context for the current thread.
|
||||
*******************************************************************************/
|
||||
public static void setObject(String key, Serializable object)
|
||||
{
|
||||
if(objectsThreadLocal.get() == null)
|
||||
{
|
||||
objectsThreadLocal.set(new HashMap<>());
|
||||
}
|
||||
objectsThreadLocal.get().put(key, object);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** remove a named object from the Context of the current thread.
|
||||
*******************************************************************************/
|
||||
public static void removeObject(String key)
|
||||
{
|
||||
if(objectsThreadLocal.get() != null)
|
||||
{
|
||||
objectsThreadLocal.get().remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get the full map of named objects for the current thread (possibly null).
|
||||
*******************************************************************************/
|
||||
public static Map<String, Serializable> getObjects()
|
||||
{
|
||||
return objectsThreadLocal.get();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fully replace the map of named objets for the current thread.
|
||||
*******************************************************************************/
|
||||
public static void setObjects(Map<String, Serializable> objects)
|
||||
{
|
||||
objectsThreadLocal.set(objects);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.Bulk
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
import com.kingsrook.qqq.backend.core.scheduler.QScheduleManager;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
@ -159,6 +160,18 @@ public class QInstanceEnricher
|
||||
}
|
||||
|
||||
enrichJoins();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// if the instance DOES have 1 or more scheduler, but no schedulable types, //
|
||||
// then go ahead and add the default set that qqq knows about //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(qInstance.getSchedulers()))
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(qInstance.getSchedulableTypes()))
|
||||
{
|
||||
QScheduleManager.defineDefaultSchedulableTypesInInstance(qInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1030,6 +1043,50 @@ public class QInstanceEnricher
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Do a default mapping from an underscore_style field name to a camelCase name.
|
||||
**
|
||||
** Examples:
|
||||
** <ul>
|
||||
** <li>word_another_word_more_words -> wordAnotherWordMoreWords</li>
|
||||
** <li>l_ul_ul_ul -> lUlUlUl</li>
|
||||
** <li>tla_first -> tlaFirst</li>
|
||||
** <li>word_then_tla_in_middle -> wordThenTlaInMiddle</li>
|
||||
** <li>end_with_tla -> endWithTla</li>
|
||||
** <li>tla_and_another_tla -> tlaAndAnotherTla</li>
|
||||
** <li>ALL_CAPS -> allCaps</li>
|
||||
** </ul>
|
||||
*******************************************************************************/
|
||||
public static String inferNameFromBackendName(String backendName)
|
||||
{
|
||||
StringBuilder rs = new StringBuilder();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// build a list of words in the name, then join them with _ and lower-case the result //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
String[] words = backendName.toLowerCase(Locale.ROOT).split("_");
|
||||
for(int i = 0; i < words.length; i++)
|
||||
{
|
||||
String word = words[i];
|
||||
if(i == 0)
|
||||
{
|
||||
rs.append(word);
|
||||
}
|
||||
else
|
||||
{
|
||||
rs.append(word.substring(0, 1).toUpperCase());
|
||||
if(word.length() > 1)
|
||||
{
|
||||
rs.append(word.substring(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** If a app didn't have any sections, generate "sensible defaults"
|
||||
*******************************************************************************/
|
||||
|
@ -30,12 +30,14 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.helpcontent.HelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
|
||||
@ -109,6 +111,8 @@ public class QInstanceHelpContentManager
|
||||
String processName = nameValuePairs.get("process");
|
||||
String fieldName = nameValuePairs.get("field");
|
||||
String sectionName = nameValuePairs.get("section");
|
||||
String widgetName = nameValuePairs.get("widget");
|
||||
String slotName = nameValuePairs.get("slot");
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// build a help content meta-data object from the record //
|
||||
@ -137,89 +141,16 @@ public class QInstanceHelpContentManager
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
if(StringUtils.hasContent(tableName))
|
||||
{
|
||||
QTableMetaData table = qInstance.getTable(tableName);
|
||||
if(table == null)
|
||||
{
|
||||
LOG.info("Unrecognized table in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
//////////////////////////
|
||||
// handle a table field //
|
||||
//////////////////////////
|
||||
QFieldMetaData field = table.getFields().get(fieldName);
|
||||
if(field == null)
|
||||
{
|
||||
LOG.info("Unrecognized table field in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
|
||||
if(helpContent != null)
|
||||
{
|
||||
field.withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
field.removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else if(StringUtils.hasContent(sectionName))
|
||||
{
|
||||
////////////////////////////
|
||||
// handle a table section //
|
||||
////////////////////////////
|
||||
Optional<QFieldSection> optionalSection = table.getSections().stream().filter(s -> sectionName.equals(s.getName())).findFirst();
|
||||
if(optionalSection.isEmpty())
|
||||
{
|
||||
LOG.info("Unrecognized table section in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
|
||||
if(helpContent != null)
|
||||
{
|
||||
optionalSection.get().withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
optionalSection.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
processHelpContentForTable(key, tableName, sectionName, fieldName, roles, helpContent);
|
||||
}
|
||||
else if(StringUtils.hasContent(processName))
|
||||
{
|
||||
QProcessMetaData process = qInstance.getProcess(processName);
|
||||
if(process == null)
|
||||
{
|
||||
LOG.info("Unrecognized process in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
processHelpContentForProcess(key, processName, fieldName, roles, helpContent);
|
||||
}
|
||||
else if(StringUtils.hasContent(widgetName))
|
||||
{
|
||||
processHelpContentForWidget(key, widgetName, slotName, helpContent);
|
||||
|
||||
if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
////////////////////////////
|
||||
// handle a process field //
|
||||
////////////////////////////
|
||||
Optional<QFieldMetaData> optionalField = CollectionUtils.mergeLists(process.getInputFields(), process.getOutputFields())
|
||||
.stream().filter(f -> fieldName.equals(f.getName()))
|
||||
.findFirst();
|
||||
|
||||
if(optionalField.isEmpty())
|
||||
{
|
||||
LOG.info("Unrecognized process field in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
|
||||
if(helpContent != null)
|
||||
{
|
||||
optionalField.get().withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
optionalField.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -230,6 +161,131 @@ public class QInstanceHelpContentManager
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
if(table == null)
|
||||
{
|
||||
LOG.info("Unrecognized table in help content", logPair("key", key));
|
||||
}
|
||||
else if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
//////////////////////////
|
||||
// handle a table field //
|
||||
//////////////////////////
|
||||
QFieldMetaData field = table.getFields().get(fieldName);
|
||||
if(field == null)
|
||||
{
|
||||
LOG.info("Unrecognized table field in help content", logPair("key", key));
|
||||
}
|
||||
else if(helpContent != null)
|
||||
{
|
||||
field.withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
field.removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else if(StringUtils.hasContent(sectionName))
|
||||
{
|
||||
////////////////////////////
|
||||
// handle a table section //
|
||||
////////////////////////////
|
||||
Optional<QFieldSection> optionalSection = table.getSections().stream().filter(s -> sectionName.equals(s.getName())).findFirst();
|
||||
if(optionalSection.isEmpty())
|
||||
{
|
||||
LOG.info("Unrecognized table section in help content", logPair("key", key));
|
||||
}
|
||||
else if(helpContent != null)
|
||||
{
|
||||
optionalSection.get().withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
optionalSection.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForProcess(String key, String processName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
if(process == null)
|
||||
{
|
||||
LOG.info("Unrecognized process in help content", logPair("key", key));
|
||||
}
|
||||
else if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
////////////////////////////
|
||||
// handle a process field //
|
||||
////////////////////////////
|
||||
Optional<QFieldMetaData> optionalField = CollectionUtils.mergeLists(process.getInputFields(), process.getOutputFields())
|
||||
.stream().filter(f -> fieldName.equals(f.getName()))
|
||||
.findFirst();
|
||||
|
||||
if(optionalField.isEmpty())
|
||||
{
|
||||
LOG.info("Unrecognized process field in help content", logPair("key", key));
|
||||
}
|
||||
else if(helpContent != null)
|
||||
{
|
||||
optionalField.get().withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
optionalField.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForWidget(String key, String widgetName, String slotName, QHelpContent helpContent)
|
||||
{
|
||||
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
|
||||
if(!StringUtils.hasContent(slotName))
|
||||
{
|
||||
LOG.info("Missing slot name in help content", logPair("key", key));
|
||||
}
|
||||
else if(widget == null)
|
||||
{
|
||||
LOG.info("Unrecognized widget in help content", logPair("key", key));
|
||||
}
|
||||
else
|
||||
{
|
||||
Map<String, QHelpContent> widgetHelpContent = widget.getHelpContent();
|
||||
if(widgetHelpContent == null)
|
||||
{
|
||||
widgetHelpContent = new HashMap<>();
|
||||
}
|
||||
|
||||
if(helpContent != null)
|
||||
{
|
||||
widgetHelpContent.put(slotName, helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
widgetHelpContent.remove(slotName);
|
||||
}
|
||||
|
||||
widget.setHelpContent(widgetHelpContent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** add a help content object to a list - replacing an entry in the list with the
|
||||
** same roles if one is found.
|
||||
|
@ -24,8 +24,11 @@ package com.kingsrook.qqq.backend.core.instances;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -33,15 +36,18 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
import com.kingsrook.qqq.backend.core.instances.validation.plugins.QInstanceValidatorPluginInterface;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
@ -50,8 +56,12 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData;
|
||||
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.QFieldMetaData;
|
||||
@ -61,16 +71,21 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
|
||||
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.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
@ -85,10 +100,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
|
||||
import org.quartz.CronExpression;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -107,6 +125,8 @@ public class QInstanceValidator
|
||||
|
||||
private boolean printWarnings = false;
|
||||
|
||||
private static ListingHash<Class<?>, QInstanceValidatorPluginInterface<?>> validatorPlugins = new ListingHash<>();
|
||||
|
||||
private List<String> errors = new ArrayList<>();
|
||||
|
||||
|
||||
@ -153,11 +173,13 @@ public class QInstanceValidator
|
||||
try
|
||||
{
|
||||
validateBackends(qInstance);
|
||||
validateAuthentication(qInstance);
|
||||
validateAutomationProviders(qInstance);
|
||||
validateTables(qInstance, joinGraph);
|
||||
validateProcesses(qInstance);
|
||||
validateReports(qInstance);
|
||||
validateApps(qInstance);
|
||||
validateWidgets(qInstance);
|
||||
validatePossibleValueSources(qInstance);
|
||||
validateQueuesAndProviders(qInstance);
|
||||
validateJoins(qInstance);
|
||||
@ -165,6 +187,8 @@ public class QInstanceValidator
|
||||
validateSupplementalMetaData(qInstance);
|
||||
|
||||
validateUniqueTopLevelNames(qInstance);
|
||||
|
||||
runPlugins(QInstance.class, qInstance, qInstance);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -183,6 +207,57 @@ public class QInstanceValidator
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void addValidatorPlugin(QInstanceValidatorPluginInterface<?> plugin)
|
||||
{
|
||||
Optional<Method> validateMethod = Arrays.stream(plugin.getClass().getDeclaredMethods())
|
||||
.filter(m -> m.getName().equals("validate")
|
||||
&& m.getParameterCount() == 3
|
||||
&& !m.getParameterTypes()[0].equals(Object.class)
|
||||
&& m.getParameterTypes()[1].equals(QInstance.class)
|
||||
&& m.getParameterTypes()[2].equals(QInstanceValidator.class)
|
||||
).findFirst();
|
||||
|
||||
if(validateMethod.isPresent())
|
||||
{
|
||||
Class<?> parameterType = validateMethod.get().getParameterTypes()[0];
|
||||
validatorPlugins.add(parameterType, plugin);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Could not find validate method on validator plugin [" + plugin.getClass().getName() + "] (to infer type being validated) - this plugin will not be used.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void removeAllValidatorPlugins()
|
||||
{
|
||||
validatorPlugins.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private <T> void runPlugins(Class<T> c, T t, QInstance qInstance)
|
||||
{
|
||||
for(QInstanceValidatorPluginInterface<?> plugin : CollectionUtils.nonNullList(validatorPlugins.get(c)))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
QInstanceValidatorPluginInterface<T> processPlugin = (QInstanceValidatorPluginInterface<T>) plugin;
|
||||
processPlugin.validate(t, qInstance, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -191,6 +266,8 @@ public class QInstanceValidator
|
||||
for(QSupplementalInstanceMetaData supplementalInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getSupplementalMetaData()).values())
|
||||
{
|
||||
supplementalInstanceMetaData.validate(qInstance, this);
|
||||
|
||||
runPlugins(QSupplementalInstanceMetaData.class, supplementalInstanceMetaData, qInstance);
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,18 +284,29 @@ public class QInstanceValidator
|
||||
if(assertCondition(StringUtils.hasContent(securityKeyType.getName()), "Missing name for a securityKeyType"))
|
||||
{
|
||||
assertCondition(Objects.equals(name, securityKeyType.getName()), "Inconsistent naming for securityKeyType: " + name + "/" + securityKeyType.getName() + ".");
|
||||
assertCondition(!usedNames.contains(name), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + name);
|
||||
|
||||
String duplicateNameMessagePrefix = "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: ";
|
||||
assertCondition(!usedNames.contains(name), duplicateNameMessagePrefix + name);
|
||||
usedNames.add(name);
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
|
||||
{
|
||||
assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + securityKeyType.getAllAccessKeyName());
|
||||
assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), duplicateNameMessagePrefix + securityKeyType.getAllAccessKeyName());
|
||||
usedNames.add(securityKeyType.getAllAccessKeyName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
|
||||
{
|
||||
assertCondition(!usedNames.contains(securityKeyType.getNullValueBehaviorKeyName()), duplicateNameMessagePrefix + securityKeyType.getNullValueBehaviorKeyName());
|
||||
usedNames.add(securityKeyType.getNullValueBehaviorKeyName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getPossibleValueSourceName()))
|
||||
{
|
||||
assertCondition(qInstance.getPossibleValueSource(securityKeyType.getPossibleValueSourceName()) != null, "Unrecognized possibleValueSourceName in securityKeyType: " + name);
|
||||
}
|
||||
|
||||
runPlugins(QSecurityKeyType.class, securityKeyType, qInstance);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -265,6 +353,8 @@ public class QInstanceValidator
|
||||
assertNoException(() -> qInstance.getTable(join.getRightTable()).getField(orderBy.getFieldName()), "Field name " + orderBy.getFieldName() + " in orderBy for join " + joinName + " is not a defined field in the right-table " + join.getRightTable());
|
||||
}
|
||||
}
|
||||
|
||||
runPlugins(QJoinMetaData.class, join, qInstance);
|
||||
});
|
||||
}
|
||||
|
||||
@ -328,6 +418,8 @@ public class QInstanceValidator
|
||||
assertCondition(StringUtils.hasContent(sqsQueueProvider.getBaseURL()), "Missing baseURL for SQSQueueProvider: " + name);
|
||||
assertCondition(StringUtils.hasContent(sqsQueueProvider.getRegion()), "Missing region for SQSQueueProvider: " + name);
|
||||
}
|
||||
|
||||
runPlugins(QQueueProviderMetaData.class, queueProvider, qInstance);
|
||||
});
|
||||
}
|
||||
|
||||
@ -342,6 +434,13 @@ public class QInstanceValidator
|
||||
{
|
||||
assertCondition(qInstance.getProcesses() != null && qInstance.getProcess(queue.getProcessName()) != null, "Unrecognized processName for queue: " + name);
|
||||
}
|
||||
|
||||
if(queue.getSchedule() != null)
|
||||
{
|
||||
validateScheduleMetaData(queue.getSchedule(), qInstance, "SQSQueueProvider " + name + ", schedule: ");
|
||||
}
|
||||
|
||||
runPlugins(QQueueMetaData.class, queue, qInstance);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -360,6 +459,8 @@ public class QInstanceValidator
|
||||
assertCondition(Objects.equals(backendName, backend.getName()), "Inconsistent naming for backend: " + backendName + "/" + backend.getName() + ".");
|
||||
|
||||
backend.performValidation(this);
|
||||
|
||||
runPlugins(QBackendMetaData.class, backend, qInstance);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -377,12 +478,33 @@ public class QInstanceValidator
|
||||
{
|
||||
assertCondition(Objects.equals(name, automationProvider.getName()), "Inconsistent naming for automationProvider: " + name + "/" + automationProvider.getName() + ".");
|
||||
assertCondition(automationProvider.getType() != null, "Missing type for automationProvider: " + name);
|
||||
|
||||
runPlugins(QAutomationProviderMetaData.class, automationProvider, qInstance);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateAuthentication(QInstance qInstance)
|
||||
{
|
||||
QAuthenticationMetaData authentication = qInstance.getAuthentication();
|
||||
if(authentication != null)
|
||||
{
|
||||
if(authentication.getCustomizer() != null)
|
||||
{
|
||||
validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class);
|
||||
}
|
||||
|
||||
runPlugins(QAuthenticationMetaData.class, authentication, qInstance);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -500,6 +622,8 @@ public class QInstanceValidator
|
||||
{
|
||||
supplementalTableMetaData.validate(qInstance, table, this);
|
||||
}
|
||||
|
||||
runPlugins(QTableMetaData.class, table, qInstance);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -897,6 +1021,11 @@ public class QInstanceValidator
|
||||
assertCondition(qInstance.getAutomationProvider(providerName) != null, " has an unrecognized providerName: " + providerName);
|
||||
}
|
||||
|
||||
if(automationDetails.getSchedule() != null)
|
||||
{
|
||||
validateScheduleMetaData(automationDetails.getSchedule(), qInstance, prefix + " automationDetails, schedule: ");
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// validate the status tracking //
|
||||
//////////////////////////////////
|
||||
@ -1233,12 +1362,17 @@ 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())
|
||||
{
|
||||
assertCondition(StringUtils.hasContent(step.getName()), "Missing name for a step at index " + index + " in process " + processName);
|
||||
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());
|
||||
}
|
||||
index++;
|
||||
|
||||
////////////////////////////////////////////
|
||||
@ -1284,13 +1418,17 @@ public class QInstanceValidator
|
||||
if(process.getSchedule() != null)
|
||||
{
|
||||
QScheduleMetaData schedule = process.getSchedule();
|
||||
assertCondition(schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null, "Either repeat millis or repeat seconds must be set on schedule in process " + processName);
|
||||
validateScheduleMetaData(schedule, qInstance, "Process " + processName + ", schedule: ");
|
||||
}
|
||||
|
||||
if(schedule.getVariantBackend() != null)
|
||||
{
|
||||
assertCondition(qInstance.getBackend(schedule.getVariantBackend()) != null, "A variant backend was not found for " + schedule.getVariantBackend());
|
||||
assertCondition(schedule.getVariantRunStrategy() != null, "A variant run strategy was not set for " + schedule.getVariantBackend() + " on schedule in process " + processName);
|
||||
}
|
||||
if(process.getVariantBackend() != null)
|
||||
{
|
||||
assertCondition(qInstance.getBackend(process.getVariantBackend()) != null, "Process " + processName + ", a variant backend was not found named " + process.getVariantBackend());
|
||||
assertCondition(process.getVariantRunStrategy() != null, "A variant run strategy was not set for process " + processName + " (which does specify a variant backend)");
|
||||
}
|
||||
else
|
||||
{
|
||||
assertCondition(process.getVariantRunStrategy() == null, "A variant run strategy was set for process " + processName + " (which isn't allowed, since it does not specify a variant backend)");
|
||||
}
|
||||
|
||||
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())
|
||||
@ -1298,12 +1436,57 @@ public class QInstanceValidator
|
||||
supplementalProcessMetaData.validate(qInstance, process, this);
|
||||
}
|
||||
|
||||
runPlugins(QProcessMetaData.class, process, qInstance);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateScheduleMetaData(QScheduleMetaData schedule, QInstance qInstance, String prefix)
|
||||
{
|
||||
boolean isRepeat = schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null;
|
||||
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");
|
||||
|
||||
if(isCron)
|
||||
{
|
||||
boolean hasDelay = schedule.getInitialDelayMillis() != null || schedule.getInitialDelaySeconds() != null;
|
||||
assertCondition(!hasDelay, prefix + " a cron schedule may not have an initial delay");
|
||||
|
||||
try
|
||||
{
|
||||
CronExpression.validateExpression(schedule.getCronExpression());
|
||||
}
|
||||
catch(ParseException pe)
|
||||
{
|
||||
errors.add(prefix + " invalid cron expression: " + pe.getMessage());
|
||||
}
|
||||
|
||||
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();
|
||||
assertCondition(first.isPresent(), prefix + " unrecognized cronTimeZoneId: " + schedule.getCronTimeZoneId());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
assertCondition(!StringUtils.hasContent(schedule.getCronTimeZoneId()), prefix + " a non-cron schedule must not specify a cronTimeZoneId");
|
||||
}
|
||||
|
||||
if(assertCondition(StringUtils.hasContent(schedule.getSchedulerName()), prefix + " is missing a scheduler name"))
|
||||
{
|
||||
assertCondition(qInstance.getScheduler(schedule.getSchedulerName()) != null, prefix + " is referencing an unknown scheduler name: " + schedule.getSchedulerName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -1404,6 +1587,8 @@ public class QInstanceValidator
|
||||
// view.getTitleFormat(); view.getTitleFields(); // validate these match?
|
||||
}
|
||||
}
|
||||
|
||||
runPlugins(QReportMetaData.class, report, qInstance);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1524,9 +1709,9 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////
|
||||
// validate field sections in the table //
|
||||
//////////////////////////////////////////
|
||||
////////////////////////////////////////
|
||||
// validate field sections in the app //
|
||||
////////////////////////////////////////
|
||||
Set<String> childNamesInSections = new HashSet<>();
|
||||
if(app.getSections() != null)
|
||||
{
|
||||
@ -1546,12 +1731,57 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// validate widgets //
|
||||
//////////////////////
|
||||
for(String widgetName : CollectionUtils.nonNullList(app.getWidgets()))
|
||||
{
|
||||
assertCondition(qInstance.getWidget(widgetName) != null, "App " + appName + " widget " + widgetName + " is not a recognized widget.");
|
||||
}
|
||||
|
||||
runPlugins(QAppMetaData.class, app, qInstance);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateWidgets(QInstance qInstance)
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(qInstance.getWidgets()))
|
||||
{
|
||||
qInstance.getWidgets().forEach((widgetName, widget) ->
|
||||
{
|
||||
assertCondition(Objects.equals(widgetName, widget.getName()), "Inconsistent naming for widget: " + widgetName + "/" + widget.getName() + ".");
|
||||
|
||||
if(assertCondition(widget.getCodeReference() != null, "Missing codeReference for widget: " + widgetName))
|
||||
{
|
||||
validateSimpleCodeReference("Widget " + widgetName + " code reference: ", widget.getCodeReference(), AbstractWidgetRenderer.class);
|
||||
}
|
||||
|
||||
if(widget instanceof ParentWidgetMetaData parentWidgetMetaData)
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(parentWidgetMetaData.getChildWidgetNameList()), "Missing child widgets for parent widget: " + widget.getName()))
|
||||
{
|
||||
for(String childWidgetName : parentWidgetMetaData.getChildWidgetNameList())
|
||||
{
|
||||
assertCondition(qInstance.getWidget(childWidgetName) != null, "Unrecognized child widget name [" + childWidgetName + "] in parent widget: " + widget.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runPlugins(QWidgetMetaDataInterface.class, widget, qInstance);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -1630,6 +1860,8 @@ public class QInstanceValidator
|
||||
}
|
||||
default -> errors.add("Unexpected possibleValueSource type: " + possibleValueSource.getType());
|
||||
}
|
||||
|
||||
runPlugins(QPossibleValueSource.class, possibleValueSource, qInstance);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.instances.validation.plugins;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullExtractStepInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** instance validator plugin, to ensure that a process which is a basepull uses
|
||||
** an extract step marked for basepulls.
|
||||
*******************************************************************************/
|
||||
public class BasepullExtractStepValidator implements QInstanceValidatorPluginInterface<QProcessMetaData>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void validate(QProcessMetaData process, QInstance qInstance, QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if there's no basepull config on the process, don't do any validation //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
if(process.getBasepullConfiguration() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// try to find an input field in the process, w/ a defaultValue that's a QCodeReference //
|
||||
// and is an instance of BasepullExtractStepInterface //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean foundBasepullExtractStep = false;
|
||||
for(QFieldMetaData field : process.getInputFields())
|
||||
{
|
||||
if(field.getDefaultValue() != null && field.getDefaultValue() instanceof QCodeReference codeReference)
|
||||
{
|
||||
try
|
||||
{
|
||||
BasepullExtractStepInterface extractStep = QCodeLoader.getAdHoc(BasepullExtractStepInterface.class, codeReference);
|
||||
if(extractStep != null)
|
||||
{
|
||||
foundBasepullExtractStep = true;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//////////////////////////////////////////////////////
|
||||
// ok, just means we haven't found our extract step //
|
||||
//////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// validate we could find a BasepullExtractStepInterface //
|
||||
///////////////////////////////////////////////////////////
|
||||
qInstanceValidator.assertCondition(foundBasepullExtractStep, "Process [" + process.getName() + "] has a basepullConfiguration, but does not have a field with a default value that is a BasepullExtractStepInterface CodeReference");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.instances.validation.plugins;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface for additional / optional q instance validators. Some will be
|
||||
** provided by QQQ - others can be defined by applications.
|
||||
*******************************************************************************/
|
||||
public interface QInstanceValidatorPluginInterface<T>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
void validate(T object, QInstance qInstance, QInstanceValidator qInstanceValidator);
|
||||
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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.logging;
|
||||
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A log message, which can be "collected" by the QCollectingLogger.
|
||||
*******************************************************************************/
|
||||
public class CollectedLogMessage
|
||||
{
|
||||
private Level level;
|
||||
private String message;
|
||||
private Throwable exception;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public CollectedLogMessage()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "CollectedLogMessage{level=" + level + ", message='" + message + '\'' + ", exception=" + exception + '}';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for message
|
||||
*******************************************************************************/
|
||||
public String getMessage()
|
||||
{
|
||||
return (this.message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for message
|
||||
*******************************************************************************/
|
||||
public void setMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for message
|
||||
*******************************************************************************/
|
||||
public CollectedLogMessage withMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for exception
|
||||
*******************************************************************************/
|
||||
public Throwable getException()
|
||||
{
|
||||
return (this.exception);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for exception
|
||||
*******************************************************************************/
|
||||
public void setException(Throwable exception)
|
||||
{
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for exception
|
||||
*******************************************************************************/
|
||||
public CollectedLogMessage withException(Throwable exception)
|
||||
{
|
||||
this.exception = exception;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for level
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Level getLevel()
|
||||
{
|
||||
return level;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for level
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setLevel(Level level)
|
||||
{
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for level
|
||||
**
|
||||
*******************************************************************************/
|
||||
public CollectedLogMessage withLevel(Level level)
|
||||
{
|
||||
this.level = level;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public JSONObject getMessageAsJSONObject() throws JSONException
|
||||
{
|
||||
return (new JSONObject(getMessage()));
|
||||
}
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.logging;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.Marker;
|
||||
import org.apache.logging.log4j.message.Message;
|
||||
import org.apache.logging.log4j.message.ObjectMessage;
|
||||
import org.apache.logging.log4j.message.SimpleMessageFactory;
|
||||
import org.apache.logging.log4j.simple.SimpleLogger;
|
||||
import org.apache.logging.log4j.util.PropertiesUtil;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QQQ log4j implementation, used within a QLogger, to "collect" log messages
|
||||
** in an internal list - the idea being - for tests, to assert that logs happened.
|
||||
*******************************************************************************/
|
||||
public class QCollectingLogger extends SimpleLogger
|
||||
{
|
||||
private List<CollectedLogMessage> collectedMessages = new ArrayList<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// just in case one of these gets activated, and left on, put a limit on how many messages we'll collect //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private int capacity = 100;
|
||||
|
||||
private Logger logger;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QCollectingLogger(Logger logger)
|
||||
{
|
||||
super(logger.getName(), logger.getLevel(), false, false, true, false, "", new SimpleMessageFactory(), new PropertiesUtil(new Properties()), System.out);
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void logMessage(String fqcn, Level level, Marker marker, Message message, Throwable throwable)
|
||||
{
|
||||
////////////////////////////////////////////
|
||||
// add this log message to our collection //
|
||||
////////////////////////////////////////////
|
||||
collectedMessages.add(new CollectedLogMessage()
|
||||
.withLevel(level)
|
||||
.withMessage(message.getFormattedMessage())
|
||||
.withException(throwable));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we've gone over our capacity, remove the 1st entry until we're back at capacity //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
while(collectedMessages.size() > capacity)
|
||||
{
|
||||
collectedMessages.remove(0);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// update the message that we log to indicate that we collected it. //
|
||||
// if it looks like JSON, insert as a name:value pair; else text. //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
String formattedMessage = message.getFormattedMessage();
|
||||
String updatedMessage;
|
||||
if(formattedMessage.startsWith("{"))
|
||||
{
|
||||
updatedMessage = """
|
||||
{"collected":true,""" + formattedMessage.substring(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
updatedMessage = "[Collected] " + formattedMessage;
|
||||
}
|
||||
ObjectMessage myMessage = new ObjectMessage(updatedMessage);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// log the message with the original log4j logger, with our slightly updated message //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
logger.logMessage(level, marker, fqcn, null, myMessage, throwable);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for logger
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setLogger(Logger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for collectedMessages
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<CollectedLogMessage> getCollectedMessages()
|
||||
{
|
||||
return collectedMessages;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void clear()
|
||||
{
|
||||
this.collectedMessages.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for capacity
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setCapacity(int capacity)
|
||||
{
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
}
|
@ -119,6 +119,34 @@ public class QLogger
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QCollectingLogger activateCollectingLoggerForClass(Class<?> c)
|
||||
{
|
||||
Logger loggerFromLogManager = LogManager.getLogger(c);
|
||||
QCollectingLogger collectingLogger = new QCollectingLogger(loggerFromLogManager);
|
||||
|
||||
QLogger qLogger = getLogger(c);
|
||||
qLogger.setLogger(collectingLogger);
|
||||
|
||||
return collectingLogger;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void deactivateCollectingLoggerForClass(Class<?> c)
|
||||
{
|
||||
Logger loggerFromLogManager = LogManager.getLogger(c);
|
||||
QLogger qLogger = getLogger(c);
|
||||
qLogger.setLogger(loggerFromLogManager);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -518,7 +546,7 @@ public class QLogger
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String makeJsonString(String message, Throwable t, List<LogPair> logPairList)
|
||||
protected String makeJsonString(String message, Throwable t, List<LogPair> logPairList)
|
||||
{
|
||||
if(logPairList == null)
|
||||
{
|
||||
@ -620,4 +648,15 @@ public class QLogger
|
||||
exceptionList.get(0).setHasLoggedLevel(level);
|
||||
return (level);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for logger
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void setLogger(Logger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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.audits;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.tables.QTableMetaData;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Object to accumulate multiple audit-details to be recorded under a single
|
||||
** audit per-record, within a process step. Especially useful if/when the
|
||||
** process step spreads its work out through multiple classes.
|
||||
**
|
||||
** Pattern of usage looks like:
|
||||
**
|
||||
** <pre>
|
||||
** // declare as a field (or local) w/ message for the audit headers
|
||||
** private AuditDetailAccumulator auditDetailAccumulator = new AuditDetailAccumulator("Audit header message");
|
||||
**
|
||||
** // put into thread context
|
||||
** AuditDetailAccumulator.setInContext(auditDetailAccumulator);
|
||||
**
|
||||
** // add a detail message for a record
|
||||
** auditDetailAccumulator.addAuditDetail(tableName, record, "Detail message");
|
||||
**
|
||||
** // in another class, get the accumulator from context and safely add a detail message
|
||||
** AuditDetailAccumulator.getFromContext().ifPresent(ada -> ada.addAuditDetail(tableName, record, "More Details"));
|
||||
**
|
||||
** // at the end of a step run/runOnePage method, add the accumulated audit details to step output
|
||||
** auditDetailAccumulator.getAccumulatedAuditSingleInputs().forEach(runBackendStepOutput::addAuditSingleInput);
|
||||
** auditDetailAccumulator.clear();
|
||||
** </pre>
|
||||
*******************************************************************************/
|
||||
public class AuditDetailAccumulator implements Serializable
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AuditDetailAccumulator.class);
|
||||
|
||||
private static final String objectKey = AuditDetailAccumulator.class.getSimpleName();
|
||||
|
||||
private String header;
|
||||
|
||||
private Map<TableNameAndPrimaryKey, AuditSingleInput> recordAuditInputMap = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AuditDetailAccumulator(String header)
|
||||
{
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setInContext()
|
||||
{
|
||||
QContext.setObject(objectKey, this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Optional<AuditDetailAccumulator> getFromContext()
|
||||
{
|
||||
return QContext.getObject(objectKey, AuditDetailAccumulator.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addAuditDetail(String tableName, QRecordEntity entity, String message)
|
||||
{
|
||||
if(entity != null)
|
||||
{
|
||||
addAuditDetail(tableName, entity.toQRecord(), message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addAuditDetail(String tableName, QRecord record, String message)
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
|
||||
if(primaryKey == null)
|
||||
{
|
||||
LOG.info("Missing primary key in input record - audit detail message will not be recorded.", logPair("message", message));
|
||||
return;
|
||||
}
|
||||
|
||||
AuditSingleInput auditSingleInput = recordAuditInputMap.computeIfAbsent(new TableNameAndPrimaryKey(tableName, primaryKey), (key) -> new AuditSingleInput(table, record, header));
|
||||
auditSingleInput.addDetail(message);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Collection<AuditSingleInput> getAccumulatedAuditSingleInputs()
|
||||
{
|
||||
return (recordAuditInputMap.values());
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void clear()
|
||||
{
|
||||
recordAuditInputMap.clear();
|
||||
}
|
||||
|
||||
|
||||
private record TableNameAndPrimaryKey(String tableName, Serializable primaryKey) {}
|
||||
}
|
@ -41,7 +41,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
/*******************************************************************************
|
||||
** Input data to insert a single audit record (with optional child record)..
|
||||
*******************************************************************************/
|
||||
public class AuditSingleInput
|
||||
public class AuditSingleInput implements Serializable
|
||||
{
|
||||
private String auditTableName;
|
||||
private String auditUserName;
|
||||
|
@ -22,7 +22,6 @@
|
||||
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;
|
||||
@ -37,9 +36,8 @@ public class ExportInput extends AbstractTableActionInput
|
||||
private Integer limit;
|
||||
private List<String> fieldNames;
|
||||
|
||||
private String filename;
|
||||
private ReportFormat reportFormat;
|
||||
private OutputStream reportOutputStream;
|
||||
private ReportDestination reportDestination;
|
||||
|
||||
private String titleRow;
|
||||
private boolean includeHeaderRow = true;
|
||||
|
||||
@ -120,71 +118,6 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -226,4 +159,36 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
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.dhatim.fastexcel.Worksheet;
|
||||
import org.apache.poi.ss.SpreadsheetVersion;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -39,15 +39,24 @@ import org.dhatim.fastexcel.Worksheet;
|
||||
*******************************************************************************/
|
||||
public enum ReportFormat
|
||||
{
|
||||
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);
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// 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);
|
||||
|
||||
|
||||
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;
|
||||
|
||||
@ -56,12 +65,16 @@ public enum ReportFormat
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
ReportFormat(Integer maxRows, Integer maxCols, Supplier<? extends ExportStreamerInterface> streamerConstructor, String mimeType)
|
||||
ReportFormat(Integer maxRows, Integer maxCols, Supplier<? extends ExportStreamerInterface> streamerConstructor, String mimeType, String extension, boolean isBinary, boolean supportsNativePivotTables, boolean supportsMultipleViews)
|
||||
{
|
||||
this.maxRows = maxRows;
|
||||
this.maxCols = maxCols;
|
||||
this.mimeType = mimeType;
|
||||
this.streamerConstructor = streamerConstructor;
|
||||
this.extension = extension;
|
||||
this.isBinary = isBinary;
|
||||
this.supportsNativePivotTables = supportsNativePivotTables;
|
||||
this.supportsMultipleViews = supportsMultipleViews;
|
||||
}
|
||||
|
||||
|
||||
@ -128,4 +141,48 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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,
|
||||
JSON,
|
||||
CSV;
|
||||
|
||||
public static final String NAME = "reportFormat";
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getPossibleValueId()
|
||||
{
|
||||
return name();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getPossibleValueLabel()
|
||||
{
|
||||
return name();
|
||||
}
|
||||
}
|
@ -22,24 +22,28 @@
|
||||
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 an Export action
|
||||
** Input for a Report action
|
||||
*******************************************************************************/
|
||||
public class ReportInput extends AbstractTableActionInput
|
||||
{
|
||||
private String reportName;
|
||||
private String reportName;
|
||||
private QReportMetaData reportMetaData;
|
||||
|
||||
private Map<String, Serializable> inputValues;
|
||||
|
||||
private String filename;
|
||||
private ReportFormat reportFormat;
|
||||
private OutputStream reportOutputStream;
|
||||
private ReportDestination reportDestination;
|
||||
|
||||
private Supplier<? extends ExportStreamerInterface> overrideExportStreamerSupplier;
|
||||
|
||||
|
||||
|
||||
@ -111,66 +115,97 @@ public class ReportInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filename
|
||||
**
|
||||
** Getter for reportDestination
|
||||
*******************************************************************************/
|
||||
public String getFilename()
|
||||
public ReportDestination getReportDestination()
|
||||
{
|
||||
return filename;
|
||||
return (this.reportDestination);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for filename
|
||||
**
|
||||
** Setter for reportDestination
|
||||
*******************************************************************************/
|
||||
public void setFilename(String filename)
|
||||
public void setReportDestination(ReportDestination reportDestination)
|
||||
{
|
||||
this.filename = filename;
|
||||
this.reportDestination = reportDestination;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportFormat
|
||||
**
|
||||
** Fluent setter for reportDestination
|
||||
*******************************************************************************/
|
||||
public ReportFormat getReportFormat()
|
||||
public ReportInput withReportDestination(ReportDestination reportDestination)
|
||||
{
|
||||
return reportFormat;
|
||||
this.reportDestination = reportDestination;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportFormat
|
||||
**
|
||||
** Getter for reportMetaData
|
||||
*******************************************************************************/
|
||||
public void setReportFormat(ReportFormat reportFormat)
|
||||
public QReportMetaData getReportMetaData()
|
||||
{
|
||||
this.reportFormat = reportFormat;
|
||||
return (this.reportMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportOutputStream
|
||||
**
|
||||
** Setter for reportMetaData
|
||||
*******************************************************************************/
|
||||
public OutputStream getReportOutputStream()
|
||||
public void setReportMetaData(QReportMetaData reportMetaData)
|
||||
{
|
||||
return reportOutputStream;
|
||||
this.reportMetaData = reportMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for reportOutputStream
|
||||
** Fluent setter for reportMetaData
|
||||
*******************************************************************************/
|
||||
public ReportInput withReportMetaData(QReportMetaData reportMetaData)
|
||||
{
|
||||
this.reportMetaData = reportMetaData;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for overrideExportStreamerSupplier
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setReportOutputStream(OutputStream reportOutputStream)
|
||||
public Supplier<? extends ExportStreamerInterface> getOverrideExportStreamerSupplier()
|
||||
{
|
||||
this.reportOutputStream = reportOutputStream;
|
||||
return overrideExportStreamerSupplier;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for overrideExportStreamerSupplier
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setOverrideExportStreamerSupplier(Supplier<? extends ExportStreamerInterface> overrideExportStreamerSupplier)
|
||||
{
|
||||
this.overrideExportStreamerSupplier = overrideExportStreamerSupplier;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for overrideExportStreamerSupplier
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ReportInput withOverrideExportStreamerSupplier(Supplier<? extends ExportStreamerInterface> overrideExportStreamerSupplier)
|
||||
{
|
||||
this.overrideExportStreamerSupplier = overrideExportStreamerSupplier;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
/*
|
||||
* 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.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Full definition of a pivot table - its rows, columns, and values.
|
||||
*******************************************************************************/
|
||||
public class PivotTableDefinition implements Cloneable
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** How a group-by (rows or columns) should be sorted.
|
||||
*******************************************************************************/
|
||||
public class PivotTableOrderBy
|
||||
{
|
||||
// todo - implement, but only if POI supports (or we build our own support...)
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** a value (e.g., field name + function) used in a pivot table
|
||||
*******************************************************************************/
|
||||
public class PivotTableValue implements Cloneable
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -60,6 +60,11 @@ public interface QueryOrGetInputInterface
|
||||
QBackendTransaction getTransaction();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
String getTableName();
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for transaction
|
||||
*******************************************************************************/
|
||||
|
@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.aggregate;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
|
||||
|
||||
@ -38,6 +39,17 @@ public class GroupBy implements Serializable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public GroupBy(QFieldMetaData field)
|
||||
{
|
||||
this.type = field.getType();
|
||||
this.fieldName = field.getName();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
|
||||
@ -346,4 +347,37 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if(o == null || getClass() != o.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QFilterCriteria that = (QFilterCriteria) o;
|
||||
return Objects.equals(fieldName, that.fieldName) && operator == that.operator && Objects.equals(values, that.values) && Objects.equals(otherFieldName, that.otherFieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(fieldName, operator, values, otherFieldName);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -371,7 +372,7 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
rs.append(")");
|
||||
|
||||
rs.append("OrderBy[");
|
||||
for(QFilterOrderBy orderBy : orderBys)
|
||||
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
|
||||
{
|
||||
rs.append(orderBy).append(",");
|
||||
}
|
||||
@ -479,4 +480,36 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if(o == null || getClass() != o.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QQueryFilter that = (QQueryFilter) o;
|
||||
return Objects.equals(criteria, that.criteria) && Objects.equals(orderBys, that.orderBys) && booleanOperator == that.booleanOperator && Objects.equals(subFilters, that.subFilters) && Objects.equals(skip, that.skip) && Objects.equals(limit, that.limit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(criteria, orderBys, booleanOperator, subFilters, skip, limit);
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ 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.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
@ -68,6 +69,24 @@ 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -569,4 +588,64 @@ 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user